agent-web-search-setup · git:20260915.ab0cd1a · 2026-09-15 · sha256 adb01b8a7a90604d

agent-web-search-setup git:20260915.ab0cd1aA

Immutable. This exact content is served forever at /api/v1/blob/adb01b8a7a90604d.

---
name: agent-web-search-setup
description: >-
  Sets up working web search on an agent whose model backend cannot run it.
  Resellers and relays that proxy Claude or Codex to another cloud (Vertex AI,
  Bedrock, or an OpenAI-compatible layer) do not execute the server-side
  web_search and web_fetch tools, so those return empty instead of failing and
  the model reports that recent things do not exist. Use this skill whenever
  searches come back with nothing, a model insists a shipped product was never
  released, web search appears to do nothing at all, someone wants to give an
  agent internet access, or the person is on a third-party base URL, a relay, or
  a 中转站. It works out which built-in tools are actually dead, removes them so
  the model stops reaching for them, installs a replacement the model can really
  call, and proves it with a live query. It configures the agent; it is not
  itself a search engine.
---

# Agent Web Search Setup

A model backend that cannot search is not the same as a model that searched and
found nothing, but from inside the conversation the two look identical. This
skill turns the first into the second, on whichever agent is in front of you.

## What actually breaks

Anthropic's `web_search` and `web_fetch` are **server tools**: the API runs them
during the request and splices results into the same response. OpenAI's Responses
API `web_search` works the same way. The client only ever declares the tool.

A reseller that proxies to another cloud cannot execute those. Anthropic's own
documentation states that web fetch is unavailable on Amazon Bedrock and Google
Cloud; search was measured failing the same way on both. What comes back is not
an error. It is an empty shell, so the model concludes the topic does not exist
and says so with total confidence. That is the whole bug, and it is why users
report their agent has "gotten stupid" rather than reporting a broken tool.

Three things follow, each of which cost real time to establish:

- **Switching models does not help.** One reseller was measured routing four
  model names to three different backends; none produced a working search.
- **`web_fetch` breaks with it.** Fixing only search moves the failure from "I
  found nothing" to "I found links but cannot open them."
- **Installing an alternative is not enough on its own.** With the dead tool
  still on the menu, a model made 21 web-search calls and 0 calls to an installed
  alternative whose description said the built-in was broken. Built-in tools win
  head-to-head. **The dead tool has to be removed, or nothing else you do
  matters.**

## 1. Prove it, on this machine

Do not take the user's word for which tool is broken, and do not take yours.

`<skill>` below is the directory holding this SKILL.md — substitute the real
path. If it is somehow not known, `find ~ -type d -name agent-web-search-setup`
locates it.

```bash
python3 <skill>/scripts/diagnose.py
```

It reads `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_MODEL`, or the
`OPENAI_` equivalents under `--api openai`, or takes `--url --key --model`. Exit
codes: `0` nothing to fix, `1` at least one tool is dead, `2` undetermined. `--json`
for parsing.

**Those values are usually not in the shell — go and read them out of the client.**
A person on a relay set it up through a provider switcher or a settings screen, not
by exporting variables, so a plain shell sees nothing and the probe stops before it
starts. For Claude Code they live in the `env` block of `~/.claude/settings.json`;
for Codex, the base URL is under `[model_providers.*]` in `~/.codex/config.toml` and
the key is in `~/.codex/auth.json`. Read them from there and pass them with `--url`
and `--key`. Never print the key.

**This step needs a working `python3`, and on a fresh Mac that is one dialog.**
`/usr/bin/python3` ships as a stub that opens the Command Line Tools installer the
first time it is called. Expect it, and say so before it appears rather than after.
The script itself is standard library only and runs on the 3.9 that macOS ships, so
nothing else has to be installed — which is exactly the difference between this one
dialog and an MCP server that wants Node plus an SDK.

It reports a verdict per tool and names the real backend from the tool-call id
prefix (`toolu_vrtx_` Vertex AI, `toolu_bdrk_` Bedrock, `ws_` an OpenAI hosted
tool). A model that simply
declined to call the tool is reported as **inconclusive**, not as broken; re-run
or try another model rather than treating silence as evidence.

**Slow is not dead, and the difference is easy to get wrong.** One gateway was
measured answering in about 15 seconds on two model ids and about 146 on a third.
A timeout set between those two numbers reports a perfectly healthy endpoint as
broken, which is the same misdiagnosis this skill exists to correct. The default
allows for the slow case; if you shorten it, a timeout still reports
inconclusive rather than a verdict.

**Codex reaches its backend through a different API, so probe that one.** Its
hosted search lives in the OpenAI Responses API, where a working endpoint returns a
`web_search_call` item. `--api openai` asks there. Asking only the Anthropic shape
leaves a Codex user with no verdict at all.

**The commonest way that probe fails is not a refusal — it is a missing route, and
that is more decisive than it looks.** A relay measured here answers the older
`/v1/chat/completions` with HTTP 200 and returns HTTP 404 for `/v1/responses`. Both
facts matter together: the endpoint really is OpenAI-compatible, and it simply does
not implement the surface hosted tools are served through. So hosted search cannot
run there at all, and for Codex that settles the whole question, because that route
is the one its hosted tools use. The probe makes this second request itself and
reports `unavailable`; a 404 with no working Chat Completions route stays
inconclusive, because then the key or the base URL is the likelier fault.

The two APIs do not share a model namespace, so `--api both` also needs
`--openai-model` (or `OPENAI_MODEL`). Without one the OpenAI probe is skipped and
says so, rather than sending a Claude model id to `/v1/responses` and collecting a
400 about the model that reads exactly like a verdict about the tool.

On the Codex side the setting is `web_search`. The values seen were `disabled`,
`cached`, `indexed` and `live`, but treat that list as a snapshot: an invalid value
makes Codex print the full set itself, which is both the fastest way to confirm the
current syntax and the only one that cannot go stale.

**If every probe comes back working, stop and look elsewhere before configuring
anything.** The tools are fine on that endpoint, so installing a replacement adds
a second way to search without fixing the first. Two causes have been seen on real
machines: another installed component answers search-shaped requests before any
tool is considered, and a client whose own setting turns search off. Both look
exactly like a dead backend from inside the conversation.

## 2. Remove the dead tools

Only the tools `diagnose.py` actually reported dead. Removing a working tool is a
regression, not a safety margin.

| Agent | Where | What |
|---|---|---|
| Claude Code, terminal or the Code surface of the desktop app | `settings.json` | add the dead tool names to `permissions.deny` |
| Codex | `~/.codex/config.toml` | `web_search = "disabled"` |
| Claude Desktop, the chat surface | **not a supported target** | see below |

**The client spells these differently from the API, and the wrong spelling fails
silently.** `diagnose.py` reports `web_search` and `web_fetch`, because that is what
the API calls them. A deny list takes `WebSearch` and `WebFetch`. Writing the API
spelling into `permissions.deny` matches no tool at all, so the file changes, the
run reports success, and the tool is still on the model's menu — the same dead end
as not editing anything. The diagnostic prints the client spelling next to its
conclusion for this reason; use that one.

A bare tool name in `permissions.deny` removes the tool from the model's context
entirely, which is what you want. Verified by reading the session's `init` event:
the tool is absent from the list the model receives, rather than present with
advice against using it.

**Merge, never replace.** These files hold the user's own configuration. Add the
one key and leave the rest byte-identical. Show them the before and after.

**Prefer pointing at a key over copying one in.** Where the client supports
reading a credential from the environment, register that reference rather than
writing the secret into a config file. These files get screenshotted into help
requests and shared wholesale; a key pasted into one travels with it.

**The desktop app is two clients, not one.** Its Code surface runs a real
`claude` binary the app downloads, carries the Claude Code settings engine, and
resolves configuration the standard way — `CLAUDE_CONFIG_DIR` when set, otherwise
`~/.claude`, with no separate configuration directory anywhere on disk. So the
Claude Code row above very probably covers it. That was not confirmed: the app
passes `CLAUDE_CONFIG_DIR` into the session it spawns and that value was not
traced, so read the session's `init` event and see the tool actually gone before
telling anyone it is fixed. Do not reach for the app's `coworkWebSearchEnabled`
preference on the way past — the app writes it from whether the feature is
available to the account rather than reading it as a setting, so editing it by
hand is overwritten on the next launch.

**Scope matters more than it looks.** `claude mcp add` takes `--scope` with
`local`, `user` or `project`, and defaults to `local` — a name that sounds
contained but writes into the user's real `~/.claude.json`. Always pass `--scope`
explicitly, and say which file you are about to change before you change it.

**The chat surface is out of scope, and saying so beats improvising.** No way to
turn its built-in search off was found, its connector flow is a series of clicks in
its own settings that no script can drive, and neither the clicks nor the menu names
are written down here — so an agent that tries will be guessing at a UI. Do not.
Tell the user plainly that this procedure does not cover that surface, and move the
work to one that it does cover: the same machine's Claude Code, in the terminal or
in the desktop app's Code surface, where every step below is a file edit.

If the gateway returns HTTP 400 on the tool definition rather than a silent
empty, removal stops being a preference and becomes a precondition: the request
fails validation before the model reads anything at all.

## 3. Install a replacement the model can really call

The replacement must be a **client-executed** tool, because that is the property
the broken ones lack. An MCP server qualifies: the agent runs it locally and the
backend only sees an ordinary function call.

**Apply that test to skill bundles too, because some of them cannot pass it.** Many
"give your agent internet access" packages are not servers at all — they are
instructions that orchestrate capabilities the model already declares. Whether that
is fatal depends entirely on which capability: a package whose instructions end up
calling the built-in `WebSearch` or `web_fetch` inherits the exact failure being
fixed here and adds a convincing layer on top of it, while one that drives a local
process — a CLI, a `curl`, a script through the shell — is unaffected.

So read how a bundle actually reaches the network before installing it. This is not
a reason to distrust bundles as a class: of the ones examined here, two installed
locally were read and both turned out to use the shell hop, one through a CLI and
one through a Python script. The class that cannot work is narrower and named in
the menu — packages that hand the model a list of search URLs and expect it to read
the results with its own fetch tool, which on this endpoint may be the dead one.

This was verified end-to-end through a gateway whose built-in search was dead,
using a random number the model could not have known. It came back
byte-identical to a direct-API control run, on two model ids that the same
gateway routes to two different clouds — so the round trip does not depend on one
vendor's passthrough happening to behave.

Read [references/backends.md](references/backends.md) for the current menu: what
each option costs the user in registration and local install, which are reachable
from where, and the one marked default. Pick from criteria, in this order:

1. **Nothing to register** beats anything requiring an account.
2. **Nothing to install locally** beats anything needing a runtime. A stock macOS
   has neither Node nor a usable `python3` — the system one is a stub that opens an
   Xcode installer dialog on first use, and every MCP server looked at here needs an
   SDK that is not in any standard library.
3. **Reachable from where the user actually is.**
4. **Answers in the language the user asks questions in.** This is not a quality
   preference. Someone who works in Chinese and gets back English sources reads that
   as the tool still being broken, which is the outcome this whole procedure exists
   to end.
5. Result quality, last. A working search beats a better search that will not run.

Do not present the whole menu to a non-technical user. Choose, tell them what you
chose and why in one sentence, and keep the rest as options if they ask.

## 4. Prove it works

Configuration written is not the deliverable. Run a query whose answer the model
cannot already know — something from this week — and show the user the real
results with real URLs. If a non-technical person cannot tell the difference
between "configured" and "working", it is not done.

**Verify with only the thing you just installed available.** If more than one
backend is registered, a working one will answer for a broken one and the check
comes back green on a configuration that is not actually working. Confirm which
tool produced the results, by name, rather than accepting that results appeared.
The same trap has bitten a shipping product, which now disables its own fallback
during a connection test for exactly this reason.

That check earns its place. The whole procedure was run against a live relay whose
built-in search this same diagnostic had just reported dead, in a session reporting
strict `default` permissions, on a machine carrying four other connected MCP
servers that could have answered instead. The transcript shows the replacement tool
called and nothing else, the run's counters show zero built-in search calls, and
the answer carried real URLs published that week. Read the transcript for the tool
name rather than skipping to the answer — an earlier run of the same steps looked
green for the wrong reason, and only the tool name showed it.

On Claude Code the isolation is two flags on the verification run, both confirmed
present on the CLI. Run the check as one command:

```bash
claude -p "search the web for news published this week about <topic> and give me the source URLs" \
  --disallowedTools WebSearch,WebFetch \
  --disable-slash-commands < /dev/null
```

`< /dev/null` is not decoration. Without it the command waits for input that never
arrives and prints a warning about stdin, which to somebody watching over the
user's shoulder looks exactly like the failure they asked you to fix.

**Do not reach for `CLAUDE_CONFIG_DIR` to get that isolation.** Credentials live in
the config directory, so pointing it at an empty one answers `Not logged in`, which
has nothing to do with search and sends the session down a wrong path. The two
flags isolate the run without touching where the login lives.

`--disallowedTools` keeps the built-ins out of the comparison and takes the client
spelling, the same one a deny list takes. `--disable-slash-commands` turns off every
installed skill, so none of them can claim the query before the new tool is
considered.

Confirm the model called the new tool by name. Results alone do not prove it:
another installed skill can claim a search-shaped request before any tool is
considered, which was measured happening on a real machine and cost a dollar of
tokens on the wrong path.

**Registering the server is not the last step. Granting it is.** A newly added MCP
server starts unapproved, and in a session running under normal permissions its
tools are refused with `Claude requested permissions to use
mcp__<server>__<tool>, but you haven't granted it yet`. The model then tells the
user it needs authorisation and stops — which, from the user's chair, is
indistinguishable from the broken search they asked you to fix. Grant it before
declaring anything done, either by running `claude` once interactively and
approving, or by adding the prefixed tool name to `permissions.allow`, which is the
same file and the same mechanism used to remove the dead tool above. The prefixed
name is what the diagnostic and the client both use, for example
`mcp__exa__web_search_exa`.

**A permission check that passes on your machine can fail on theirs, and this one
did.** Two runs of the identical configuration diverged: one where the session
reported `permissionMode: auto` called the tool freely, and one where it reported
`permissionMode: default` had the same call refused. The difference was the
credential — pointing the client at a relay token drops a session out of whatever
relaxed mode a logged-in account was enjoying. So run the verification under the
configuration the user actually has, relay token included, or the green you get
belongs to your machine and not to theirs.

**`claude mcp list` is not the readiness check either.** It reports a status, not a
capability, and it kept saying `Pending approval` in both runs — the one where the
tool answered and the one where it was refused. An agent that treats the listing as
verification will reinstall something that is already installed, and will still not
know whether it works. The live query, with the tool name read back from the
transcript, is the only check that answers the question being asked.

**What to look for is a trigger list, not a name.** Read the installed skills'
front matter and find any whose triggers include bare search words — "search",
"find", "搜", "查". One globally installed router was confirmed carrying exactly
that list, so on that machine every verification query would have been claimed
before an MCP tool was considered. `--disable-slash-commands` settles it for the
check itself; for daily use the user has to know the other skill is there.

Search in the language the answer lives in. A Chinese question about a Chinese
product returns better sources asked in Chinese.

## What done looks like

- `diagnose.py` reported which tools were dead, and only those were removed.
- A replacement is registered **and granted** — the model calls it and is not
  refused for permission.
- A live query returned real, current, citable URLs, shown to the user.
- The transcript names the tool that answered, and it is the new one.
- That query ran under the configuration the user actually has, relay credential
  included, not under a looser one that happened to be on this machine.
- Every file touched was merged into, not overwritten, and the user saw the diff.

## Reference

- [references/backends.md](references/backends.md) — the option menu, what each
  costs, and the current default.