ruvnet/ruflo · error · HttpFetchValidationError
PRIVATE_ADDRESS
PRIVATE_ADDRESS
Error message
host ${host} is loopback/private/link-local; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 to override What it means
HttpFetchValidationError with code PRIVATE_ADDRESS, thrown by validateUrl() (http-fetch-tools.ts:63) when the URL's host is loopback ('localhost', '::1'), RFC-1918 private (10.x, 172.16-31.x, 192.168.x), link-local (169.254.x, fe80:), or IPv6 ULA (fc/fd prefix). This is an SSRF guard: an MCP-served fetch tool must not be redirectable at internal services, cloud metadata endpoints, or the user's own machine. It can be deliberately lifted with CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1.
Solutions
- If internal fetching is intended and the environment is trusted, set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 in the MCP server's environment and restart it
- Otherwise expose the target through a public endpoint or an authenticated tunnel
- Keep the override scoped to the specific environment — do not bake it into shared images
Example fix
# before
$ claude-flow mcp start
# http_fetch { url: 'http://localhost:3000/api' }
# -> PRIVATE_ADDRESS: host localhost is loopback/private/link-local
# after (deliberate, trusted local dev)
$ CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 claude-flow mcp start
# http_fetch { url: 'http://localhost:3000/api' } -> 200 Defensive patterns
Strategy: validation
Validate before calling
function isPrivateHost(host: string): boolean {
const h = host.toLowerCase();
if (h === 'localhost' || h === '::1' || h === '[::1]') return true;
if (/^10\./.test(h) || /^192\.168\./.test(h)) return true;
if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
if (/^169\.254\./.test(h)) return true;
if (h.startsWith('fc') || h.startsWith('fd') || h.startsWith('fe80:')) return true;
return false;
}
const host = new URL(url).hostname;
const allowPrivate = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE === '1';
if (isPrivateHost(host) && !allowPrivate) {
throw new Error('target is private/loopback; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 if intended');
} Try / catch
try {
await callTool('http_fetch', { url });
} catch (e) {
if (e instanceof HttpFetchValidationError && e.code === 'PRIVATE_ADDRESS') {
// deliberate local-dev fetch: retry with the override set in the server env, or fall back to your own fetch
return fetchLocal(url);
}
throw e;
} Prevention
- For local/internal targets, launch the MCP server with CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 deliberately, never globally by default
- Do not use the fetch tool to reach cloud metadata or internal admin surfaces — the block is the SSRF guard working
- Keep the override scoped to dev environments only
When it happens
Trigger: http_fetch against 'http://localhost:3000/api', 'http://127.0.0.1:8080', 'http://192.168.1.10/admin', 'http://10.0.4.7/health', or a docker/k8s-internal service name that resolves to a private range — with CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE unset or not '1'. DNS names resolving to private IPs are caught by the host check when the hostname itself is private; numeric checks run on the literal host.
Common situations: Developers testing against a local dev server; fetching an internal service in docker-compose or k8s; CI where the target is an internal endpoint; users surprised the tool will not reach their own laptop's server.
Related errors
- FORBIDDEN_HEADER
- FORBIDDEN_PROTOCOL
- Resolved IP for is internal ( )
- Dangerous key segment rejected
- Failed to fetch base servers
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/0d68f30a14aa785f.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-tools/http-fetch-tools.ts:63
*/
export function validateUrl(rawUrl: string): URL {
let parsed: URL;
try {
parsed = new URL(rawUrl);
} catch {
throw new HttpFetchValidationError(`invalid URL: ${rawUrl}`, 'INVALID_URL');
}
const proto = parsed.protocol.toLowerCase();
if (proto !== 'http:' && proto !== 'https:') {
throw new HttpFetchValidationError(
`protocol ${parsed.protocol} not allowed (only http: and https:)`,
'FORBIDDEN_PROTOCOL',
);
}
const host = parsed.hostname.toLowerCase();
const allowPrivate = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE === '1';
if (!allowPrivate && isPrivateOrLoopback(host)) {
throw new HttpFetchValidationError(
`host ${host} is loopback/private/link-local; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 to override`,
'PRIVATE_ADDRESS',
);
}
return parsed;
}
function isPrivateOrLoopback(host: string): boolean {
if (host === 'localhost' || host === 'localhost.localdomain') return true;
// IPv6 loopback
if (host === '::1' || host === '[::1]') return true;
// IPv4 numeric checks
const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
if (m) {
const a = Number(m[1]);
const b = Number(m[2]);
if (a === 0) return true; // 0.0.0.0/8
if (a === 127) return true; // loopbackView on GitHub (pinned to fa13ee4ad6)