A common pattern: validate a configuration URL before using it.

try {
  const parsedBase = new URL(BASE);
  if (parsedBase.protocol !== "https:") {
    console.error("Base URL must use HTTPS");
    process.exit(1);
  }
} catch {
  console.error("Invalid base_url");
  process.exit(1);
}

This looks correct. The URL constructor either throws on invalid input or returns a parsed object whose protocol can be checked. Both branches are handled. The code then proceeds to use BASE (the original string) for HTTP requests, prompt interpolation, and other downstream work.

The flaw is that the URL constructor's idea of "valid" is more permissive than the code assumes. JavaScript's URL parser accepts strings that have embedded newlines and other ASCII control characters. It strips them from the parsed result silently — but it does NOT throw on input that contained them.

So a config with

"base_url": "https://api.example.com\nINJECTED INSTRUCTIONS"

passes validation. The parsed URL's href is https://api.example.cominjected/ (newline collapsed, "INJECTED" silently appended to the host because of how URL parsing handles whitespace + path delimiters). But the code never reads parsedBase.href — it keeps using BASE, the original multi-line string.

When BASE is then interpolated into the assembled system prompt:

## API CONNECTION
Base URL: ${BASE}

The output is two lines instead of one:

## API CONNECTION
Base URL: https://api.example.com
INJECTED INSTRUCTIONS

The model treats the second line as new prompt content. The injection escaped.

The threat model

This isn't a remote-attacker bug. The base_url is read from a local config file that's owned by the user running the tool. A malicious actor would have to write to the user's home directory, at which point they have lots of other attack surfaces.

The realistic threat is misconfiguration. A copy-paste from a documentation page that included a soft line break in the URL string. A corrupted byte during a file write. An install script that templated the URL into config.json and didn't trim. Any of these can land a multi-line base_url in the config without anyone noticing — because the validation accepted it.

For a tool whose behavior is gated by the system prompt, "stray bytes can become new prompt lines" is the failure mode. It doesn't matter whether the source is adversarial; it matters that the prompt's structural invariants can be broken by data the validator was supposed to reject.

The fix

Use the parser's output. After validation succeeds, replace BASE with the canonical href:

let parsedBase;
try {
  parsedBase = new URL(BASE);
  if (parsedBase.protocol !== "https:") {
    console.error("Base URL must use HTTPS");
    process.exit(1);
  }
} catch {
  console.error("Invalid base_url");
  process.exit(1);
}
if (parsedBase.href !== BASE && parsedBase.href.replace(/\/$/, "") !== BASE.replace(/\/$/, "")) {
  console.error(`  base_url canonicalized: ${BASE} → ${parsedBase.href}`);
}
BASE = parsedBase.href.replace(/\/$/, "");

The URL constructor already does the canonicalization. The fix is just to use the result. After this, BASE is always a single-line normalized URL. If the canonical form differs from the input, the warning surfaces that to the user so they can fix the config.

Defense-in-depth

One layer of defense is rarely enough for security-relevant invariants. Even after BASE is canonicalized, the prompt assembly should still sanitize interpolated values before they land in the output. A small helper:

function _safeInterp(v) {
  if (v === null || v === undefined) return "";
  return String(v)
    .replace(/[\x00-\x1F\x7F]+/g, "")     // strip control chars
    .replace(/\{\{([A-Z_]+)\}\}/g, "{ {\$1} }")  // defang templates
    .slice(0, 500);
}

Applied to every interpolated value at the prompt-assembly boundary. If a future code path bypasses the BASE canonicalization, or if a value flows through a different validation that misses newlines, the sanitizer catches it at the last step before output.

The defang of {{TEMPLATE}} markers handles a different attack: an attacker who plants a fake placeholder string in the base_url ({{ADMIN_OVERRIDE}}) hoping the template engine will resolve it. The template pass has already run by that point so the placeholder is inert, but rendering the literal text into the prompt is still confusing for the model. Defanging to { {ADMIN_OVERRIDE} } makes it obviously not-a-template.

The general lesson

Validation that doesn't normalize is half a validator. The check "is this parseable?" is necessary but not sufficient; the next question is "what does it parse to?" If the parsed result differs from the input, downstream code that uses the input is operating on data the validator never blessed.

For URLs specifically: use new URL(x).href, not the original x, for everything downstream. The same principle applies to dates (new Date(x).toISOString()), to JSON (parse and re-serialize), to any other format with a canonicalization step. If the canonicalization exists, use it; if you keep the unnormalized form around, you're carrying whatever weirdness the input had.

The version on the install URL is 4.5.111.