fetch_external work as a general-purpose API proxy. Instead of pasting tokens into every prompt, or building a dedicated connector for each API, you map a domain to an environment variable. When Claude calls fetch_external with a URL, Reacher looks up that domain, reads the token from the server environment, and injects it into the request automatically.
Claude sends the URL. Reacher sends the credential. Claude never sees the token.
The problem it solves
Without token injection, every API call would require one of:- Pasting the token into the Claude conversation (exposed in chat history, prompt injections possible)
- Building a dedicated MCP tool per API (maintenance overhead, still needs token storage)
- Storing tokens client-side in Claude Desktop config (not available in Claude.ai)
.env. Every subsequent call to that domain gets it injected transparently.
How FETCH_EXTERNAL_TOKEN_MAP works
FETCH_EXTERNAL_TOKEN_MAP is a JSON string in your .env that maps domain names to environment variable names:
.env
gist_kb and fetch_external both use GITHUB_TOKEN) without duplicating it.
The token injection flow
Claude calls fetch_external with a URL
Claude constructs a No token, no credentials. Just a URL and a method.
fetch_external call and sends it to the MCP server:Handler parses the hostname
The handler extracts the hostname from the URL using the built-in
URL class:src/tools/fetch_external.js
Domain is checked against the allowlist
Before any token lookup, the domain is verified against If the domain is not in the allowlist, the request is rejected immediately — no network call, no token lookup.
PROXY_ALLOWED_DOMAINS:src/tools/fetch_external.js
Hostname is looked up in the token map
The token map is loaded once at module initialization from Then the hostname is looked up to find which env var holds its token:
FETCH_EXTERNAL_TOKEN_MAP:src/tools/fetch_external.js
src/tools/fetch_external.js
Token is read and injected as a header
If the lookup finds a variable name, the token is read from
env and injected into the request headers:src/tools/fetch_external.js
env here is process.env — the token value is only ever read server-side. It is never returned to Claude in any response.Request is made with the injected header
The assembled request goes out with the full headers:The upstream API receives
src/tools/fetch_external.js
Authorization: Bearer ghp_xxx as if a human had set it manually.Response is returned to Claude — token stripped
The response body, status, and headers are returned to Claude. The
Authorization header is never echoed back — Claude only sees the API’s response data, not the credential used to obtain it.The audit log also strips token values before writing, so they do not appear in reacher-audit.log.Custom header formats
The default injection usesAuthorization: Bearer <token>. Some APIs use different authentication schemes. You have two options:
Pass the header manually from Claude
For one-off requests, Claude can set a customAuthorization header directly:
Add a custom header format in the tool
For APIs that use a scheme other than Bearer (Jira Basic auth, LinearAuthorization: <token> without “Bearer”, etc.), modify fetch_external.js to detect those domains and format the header accordingly.
Here is an example that adds support for Jira’s Base64 Basic auth:
src/tools/fetch_external.js
.env:
.env
api-key style headers (used by some services like Datadog or Algolia), the same pattern applies — change Authorization: Bearer to the appropriate header name and format:
Adding a new token mapping
Add the domain to PROXY_ALLOWED_DOMAINS
.env
fetch_external will refuse to call any domain not in this list, regardless of what’s in the token map. Both values must be set.Add the domain-to-variable mapping in FETCH_EXTERNAL_TOKEN_MAP
.env
Restart the server
The token map is loaded at startup (
JSON.parse(process.env.FETCH_EXTERNAL_TOKEN_MAP || '{}')). Changes to .env require a restart to take effect.Verify the integration
Ask Claude to make a test call to the new API:
“Call the Linear API at https://api.linear.app/graphql with a query for my assigned issues.”If the token is injected correctly, you’ll get a valid API response. If it fails with a 401, check that the hostname in the token map exactly matches the hostname in the URL — including subdomains.
PROXY_ALLOWED_DOMAINS and FETCH_EXTERNAL_TOKEN_MAP interaction
These two settings are independent but complementary:| Scenario | PROXY_ALLOWED_DOMAINS | FETCH_EXTERNAL_TOKEN_MAP | Outcome |
|---|---|---|---|
| Domain in both | api.github.com | {"api.github.com":"GITHUB_TOKEN"} | Request made with injected token |
| Domain in allowlist only | api.github.com | {} | Request made with no auth header |
| Domain in map only | (not listed) | {"api.github.com":"GITHUB_TOKEN"} | Request blocked — domain not allowed |
| Domain in neither | (not listed) | {} | Request blocked |
PROXY_ALLOWED_DOMAINS without a corresponding entry in FETCH_EXTERNAL_TOKEN_MAP.
Security properties
The token injection design provides these guarantees:
process.env inside the handler and written to a request header. It is never included in any MCP response, never logged to reacher-audit.log, and never visible in the Claude conversation.
Claude cannot exfiltrate tokens. Claude can call fetch_external with any URL — but it cannot read what token was injected, because the handler does not return that information. The audit log confirms the call happened, not the credential value.
The allowlist prevents open-proxy abuse. Even with a token map configured, fetch_external will not proxy requests to arbitrary domains. The domain check happens before the token lookup, so a misused call fails before any credentials are touched.
Token rotation requires no code changes. Rotating a credential is a one-line .env update followed by a server restart. The token map does not change — only the value of the referenced env var does.