santifer/career-ops · error · Error

plugin egress to "${u.hostname}" is not in allowedHosts [${[

Error message

plugin egress to "${u.hostname}" is not in allowedHosts [${[...allow].join(', ')}]

What it means

Thrown by the guardedFetch host check when the manifest declares a non-empty allowedHosts allowlist and the request's hostname is not in it. The allowlist pins each plugin to exactly the hosts it needs (least privilege); the error lists the allowed set so the gap is obvious.

Source

Thrown at plugins/_engine.mjs:377

 * the allowlist + per-hop re-validation + cross-host credential strip bound it.
 *
 * ADVISORY only: this binds a plugin that routes through ctx.fetch*, not one
 * that calls global fetch directly (see the trust note in README.md).
 *
 * @param {string[]} allowedHosts
 */
function makeGuardedFetch(allowedHosts, { allowsLocalhost = false } = {}) {
  const allow = new Set(allowedHosts);
  const isLoopbackHost = (h) => /^(localhost|127\.\d+\.\d+\.\d+|\[?::1\]?)$/i.test(h);
  const hostOk = (u) => {
    if (u.protocol !== 'https:') {
      // Plain HTTP is allowed ONLY for an opted-in loopback host (local-AI
      // providers like Ollama/LM Studio serve http://localhost:11434).
      if (!(allowsLocalhost && u.protocol === 'http:' && isLoopbackHost(u.hostname))) {
        throw new Error(`plugin egress must use HTTPS: ${u.href}`);
      }
    }
    if (allow.size > 0 && !allow.has(u.hostname)) throw new Error(`plugin egress to "${u.hostname}" is not in allowedHosts [${[...allow].join(', ')}]`);
  };
  return async function guardedFetch(url, opts = {}) {
    const { timeoutMs = 10_000, headers = {}, method = 'GET', body = null } = opts;
    let current = new URL(url);
    hostOk(current);
    // SSRF: reject a host that resolves to a private/loopback/metadata address
    // (re-checked on every redirect hop). Loopback allowed only when opted in.
    await resolveAndValidate(current.hostname, { allowsLocalhost });
    let reqHeaders = { ...headers };
    for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), timeoutMs);
      let res;
      try {
        res = await fetch(current.href, {
          method, headers: reqHeaders, body, redirect: 'manual', signal: controller.signal,
        });
      } finally {

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Add the missing hostname to the plugin manifest's allowedHosts (if the call is legitimate).
  2. Fix the plugin code to call the already-allowed host instead.
  3. Check redirects — the guard re-validates each hop, so a redirect target must also be allowlisted.
  4. Re-audit the plugin's network surface before widening the allowlist.

Example fix

// before: allowedHosts = ['api.example.com'], code hits cdn.example.com
await ctx.fetch('https://cdn.example.com/asset');
// after: add cdn.example.com to manifest allowedHosts, or use the api host
Defensive patterns

Strategy: validation

Validate before calling

function isAllowedHost(s, allowedHosts) {
  const allow = new Set(allowedHosts);
  if (allow.size === 0) return true;
  return allow.has(new URL(s).hostname);
}

Type guard

/** Narrows a URL to one whose hostname is in the plugin's allowlist. */
function isAllowedHost(s, allowedHosts) {
  if (typeof s !== 'string') return false;
  const allow = new Set(allowedHosts);
  if (allow.size === 0) return true;
  let u;
  try { u = new URL(s); } catch { return false; }
  return allow.has(u.hostname);
}

Prevention

When it happens

Trigger: A plugin requests a host not declared in its manifest's allowedHosts; a redirect hops to an allowlisted-external host; the manifest lists api.example.com but the code hits cdn.example.com.

Common situations: Manifest allowlist out of date vs the plugin's actual calls; plugin hits a CDN or sibling subdomain not enumerated; a redirect leaves the allowed host set; typo in the allowlist entry.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/89fd92abe7275459. Report an issue: GitHub.