# Web tools

> kin can read the open web through three tools. One works out of the box; the other two need a Brave Search key.

`kin` can read the open web through three tools. One works out of the box; the other two need a Brave Search key.

<!-- SOURCE: src/kin/harness/tools/web_fetch.py, src/kin/harness/tools/web_search.py, src/kin/harness/tools/web_context.py, src/kin/harness/tools/cite_check.py, src/kin/harness/tools/__init__.py -->

## Enabling

`web_fetch` is always available. The two Brave-backed tools, `web_search` and `web_context`, are registered **only when a Brave key is present** — so the model is never offered a tool it cannot use.

The easiest way to add a key is from inside kin: **`/brave-key`** (alias `/search-key`) opens the [search-key picker](/docs/kin/guide/providers/#search-credentials-stay-separate) pre-selected on its **Brave Search** row with the masked key field focused — paste the key, hit ++enter++, and the key is saved to Kin's 0600 `~/.kin/credentials.toml` store with `web_search` + `web_context` registered **live** (no restart). The same row lists where to get a key. That picker offers search rows only; model credentials live in `/models`.

The key is read from, in order:

| Source | Key |
|---|---|
| Environment | `BRAVE_API_KEY` |
| Environment | `KIN_BRAVE_API_KEY` |
| Global credential store | `brave_api_key` in `~/.kin/credentials.toml` |

`brave_api_key` is a global-only setting (it is stripped from a project file), like other secrets. See [Configuration](/docs/kin/getting-started/configuration/) and [Environment variables](/docs/kin/reference/environment-variables/).

```bash
BRAVE_API_KEY=BSA... uv run kin
```

With no key set, only `web_fetch` appears in the toolset.

## web_search vs web_context vs web_fetch

The three tools cover three jobs. Pick by what you want back:

| Tool | Returns | Use when |
|---|---|---|
| `web_search` | A ranked list of results — title, URL, snippet, date. | You want links to choose from, then read one with `web_fetch`. |
| `web_context` | Pre-extracted, re-ranked readable snippets per URL (Brave LLM Context). | You want grounding context for a question, not just links. |
| `web_fetch` | The main content of one specific URL as markdown (or extracted PDF text). | You already have a URL — a search result, or one you were given. |

`web_search` takes a `query`, an optional `count` (1–20, default 10), and an optional `freshness` filter (`pd`/`pw`/`pm`/`py` or a `YYYY-MM-DDtoYYYY-MM-DD` range). `web_context` takes a `query`, a `count` of results to consider (1–50, default 5), and a `max_tokens` budget (1024–32768, default 8192). Both use the same Brave Search plan and key.

`web_fetch` takes a `url` and an optional `max_chars` cap (default 50,000). It routes by content type: HTML is extracted to markdown, PDFs are extracted to text, and other text types (JSON, XML, CSV, …) pass through verbatim.

A common pattern is `web_search` to find candidates, then `web_fetch` to read the best one. When you only need grounding, `web_context` does both in one call.

None of the three can interact with a page — no clicking, no filling a form, no waiting on JS-rendered content. For that, see the [browser](/docs/kin/guide/browser/) tool.

A fourth, narrower web tool, `cite_check`, verifies that a batch of already-cited URLs still resolve (HEAD-first, never downloading bodies, with a Wayback-snapshot substitute for dead ones). It is always registered, reuses `web_fetch`'s SSRF guard on every hop, and backs the [deep-research citation-liveness gate](/docs/kin/guide/deep-research/#the-citation-liveness-gate).

## SSRF safety

`web_fetch` only reaches public internet hosts. It blocks internal and loopback targets, so the model cannot use it to probe your network or a cloud metadata endpoint.

Concretely, the tool:

- Allows only `http` and `https` URLs.
- Resolves the hostname and validates **every** resolved address, rejecting loopback, link-local (including `169.254/16` cloud metadata), private, CGNAT, multicast, reserved, and unspecified ranges — and unwraps IPv4-mapped IPv6 so a wrapped loopback cannot slip through.
- Connects to the validated, pinned IP while preserving the `Host` header and TLS SNI, so a DNS-rebinding response cannot redirect the connection to an internal address after the check.
- Follows redirects manually and re-validates every hop, and caps the response size and redirect count.

A blocked address returns a plain `error: blocked for safety` result the model reads and moves on from — it is not a crash.

All three tools frame their output as untrusted data before it enters the conversation, since titles, snippets, and page bodies are attacker-influenceable. For how that framing works, see the "Untrusted content" section of [REFERENCE.md](https://github.com/kinra-ai/kin/blob/dev/REFERENCE.md).
