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
Thrown by validateUrl as an SSRF guard: the parsed hostname resolves to loopback, RFC-1918 private space, link-local, CGNAT, or IPv6 ULA/loopback, and the env override CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE is not set to '1'. This secure-by-default posture stops the fetch tool from reaching internal services. The error carries code PRIVATE_ADDRESS.
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 6b01dc5a68)
Solutions
- Set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 in the environment when fetching local/internal targets is intentional.
- Route the fetch through a public-facing endpoint (tunnel, ingress) instead of the private address.
- Confirm you actually need a private target — the default is a security control, not a bug.
- For one-off local testing: `CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 npx @claude-flow/cli ...`.
Example fix
// before
http_fetch({ url: 'http://localhost:3000/health' }) // throws PRIVATE_ADDRESS
// after
// set env: export CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1
http_fetch({ url: 'http://localhost:3000/health' }) Defensive patterns
Strategy: validation
Validate before calling
function assertPublicUrl(raw, allowPrivate = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE === '1') {
const u = new URL(raw);
const host = u.hostname.toLowerCase();
if (!allowPrivate && isPrivate(host)) {
throw new Error(`private host ${host}; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1`);
}
return u;
} Prevention
- Set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 only for trusted local/internal targets.
- Use a public ingress/tunnel instead of the private address when possible.
- Treat the default as a security control, not a nuisance.
When it happens
Trigger: URLs targeting localhost, 127.0.0.1, ::1, 10.x, 172.16-31.x, 192.168.x, 169.254.x, 100.64-127.x (CGNAT), fc/fd (IPv6 ULA), fe80: (link-local), or 0.0.0.0 — without CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 in the environment.
Common situations: Fetching a local dev server (http://localhost:3000); hitting an internal service on a private subnet; cloud metadata endpoint 169.254.169.254; testing against a Docker bridge IP; CI environments where the target is on a private network.
Related errors
- SSRF guard: private/loopback host rejected — ${host}
- SSRF guard: private/loopback host rejected — ${host}
- Resolved IP for ${hostname} is internal (${address})
- FORBIDDEN_PROTOCOL
- FORBIDDEN_HEADER
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/0d68f30a14aa785f.
Report an issue: GitHub.