ruvnet/ruflo · error · HttpFetchValidationError
FORBIDDEN_HEADER
FORBIDDEN_HEADER
Error message
header "${key}" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1 What it means
HttpFetchValidationError with code FORBIDDEN_HEADER, thrown by validateHeaders() (http-fetch-tools.ts:103) when a request header is credential-bearing: exactly 'authorization', 'cookie', 'set-cookie', or 'proxy-authorization', or any header starting with 'x-auth-' / 'x-api-key'. These are blocked unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1, so secrets are not exfiltrated through an MCP fetch tool (tool arguments are commonly logged and visible to the agent host).
Solutions
- If sending credentials from this tool is intended and the environment is controlled, set CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1 in the MCP server env and restart
- Prefer routing authenticated calls through your own service so secrets never enter tool arguments
- Never paste raw tokens into tool args even when allowed — they can be logged in transcripts
Example fix
# before
# http_fetch { url: 'https://api.example.com/me', headers: { Authorization: 'Bearer ...' } }
# -> FORBIDDEN_HEADER: header "Authorization" is not allowed
# after (deliberate, trusted environment)
$ CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1 claude-flow mcp start
# http_fetch { url: '...', headers: { Authorization: 'Bearer ...' } } -> ok Defensive patterns
Strategy: validation
Validate before calling
const FORBIDDEN_EXACT = new Set(['authorization', 'cookie', 'set-cookie', 'proxy-authorization']);
function isCredentialHeader(name: string): boolean {
const lower = name.toLowerCase();
return FORBIDDEN_EXACT.has(lower) || lower.startsWith('x-auth-') || lower.startsWith('x-api-key');
}
const allowAuth = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH === '1';
const headerNames = Object.keys(headers ?? {});
if (!allowAuth && headerNames.some(isCredentialHeader)) {
throw new TypeError('credential headers require CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1');
} Try / catch
try {
await callTool('http_fetch', { url, headers });
} catch (e) {
if (e instanceof HttpFetchValidationError && e.code === 'FORBIDDEN_HEADER') {
// fall back: strip the credential header and call an unauthenticated endpoint, or proxy via your own service
const safeHeaders = Object.fromEntries(Object.entries(headers).filter(([k]) => !isCredentialHeader(k)));
return callTool('http_fetch', { url, headers: safeHeaders });
}
throw e;
} Prevention
- Route authenticated API calls through your own backend; keep secrets out of tool arguments entirely
- If credential headers are required, start the MCP server with CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1 in a controlled environment
- Never paste raw bearer tokens into tool args — transcripts and logs retain them
When it happens
Trigger: Calling http_fetch with headers: { Authorization: 'Bearer sk-...' }, { Cookie: 'session=...' }, { 'x-api-key': '...' }, or { 'X-Auth-Token': '...' } while CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH is not '1'. The check is case-insensitive (keys are lowercased) and prefix-based for x-auth-* and x-api-key*.
Common situations: Treating http_fetch like curl/fetch and pasting an API key header out of habit; calling internal APIs that require auth; CI pipelines forwarding credentials; users unaware the guard exists and confused why 'normal' headers fail.
Related errors
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/2d24a76fc157b0b6.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-tools/http-fetch-tools.ts:103
if (a === 169 && b === 254) return true; // link-local
if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT
return false;
}
// IPv6 bracketed addresses and other special forms — be conservative and
// accept only public-looking literals. Reject obvious private/local forms.
if (host.startsWith('fc') || host.startsWith('fd')) return true; // fc00::/7 ULA
if (host.startsWith('fe80:')) return true; // link-local
return false;
}
export function validateHeaders(headers: Record<string, string>): Record<string, string> {
const allowAuth = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH === '1';
const out: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
const lower = key.toLowerCase();
if (!allowAuth) {
if ((FORBIDDEN_HEADERS_EXACT as readonly string[]).includes(lower)) {
throw new HttpFetchValidationError(
`header "${key}" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1`,
'FORBIDDEN_HEADER',
);
}
if (FORBIDDEN_HEADER_PREFIXES.some((p) => lower.startsWith(p))) {
throw new HttpFetchValidationError(
`header "${key}" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1`,
'FORBIDDEN_HEADER',
);
}
}
if (typeof value !== 'string') {
throw new HttpFetchValidationError(
`header "${key}" must be a string`,
'INVALID_HEADER_VALUE',
);
}
out[key] = value;View on GitHub (pinned to fa13ee4ad6)