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

  1. 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
  2. Prefer routing authenticated calls through your own service so secrets never enter tool arguments
  3. 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

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)