Kotoba

Where to start

What Kotoba isWho she is and where she runs, in one page
InstallingOne package, two commands
First runA key, a model, her first words
The approval gateHow she asks before she acts
The two voice modesLocal voice, or the agent tunnel
The soul fileChange who she is
RoadmapWhat grows next, and what was cut on purpose

Or a section

↑↓ move openesc close124 pages
All pages

Security9 of 10

Fetching and third-party content

About 5 minutes to read

On this page

Two guards sit between the outside world and the agentic loop. Both are filters, and both are described here as filters rather than boundaries.

The URL guard

Anything that fetches a URL the model can influence goes through one function first. There are four callers:

  • web_extract — reading a page
  • remember_image — saving an image she was shown
  • the Live2D model download
  • connecting a remote MCP server, whose URL comes off a public registry

Without a guard, "read http://169.254.169.254/…" — cloud instance metadata, and the credentials behind it — or any internal endpoint would be reachable through Kotoba from the outside.

What it allows

Only http and https. Every other scheme is refused by name.

Then the hostname is resolved to all its addresses, and the fetch is refused if any of them is:

  • loopback
  • private (RFC 1918 and equivalents)
  • link-local — which is what covers 169.254.0.0/16 and fe80::/10
  • reserved
  • multicast
  • unspecified

An IPv4-mapped IPv6 address is unwrapped and re-checked. An address that cannot be parsed is treated as unsafe. A hostname that cannot be resolved is refused rather than fetched: what cannot be vetted is not fetched.

Four names are blocked outright as a cheap first cut, before resolution: localhost, metadata.google.internal, metadata, instance-data.

Redirects

Redirects are not followed automatically. web_extract, remember_image and the model download each follow them manually so that every hop is checked again. A redirect to a blocked address stops there.

What it does not cover

A true DNS rebind stays open. The name is resolved to vet it, and resolved again to connect; a resolver that answers differently between those two moments defeats the check. This is stated in the code and is not fixed by it.

One deliberate opt-out exists. KOTOBA_ALLOW_LOCAL_MCP=1 disables the guard for remote MCP server URLs only, so you can connect a server you run yourself on localhost or the LAN. It affects nothing else, and stdio servers never carry a URL. Leave it unset unless you need it, because the URL of a discovered server is exactly the model-influenced input the guard exists for.

The prompt-injection screen on MCP tools

An MCP server supplies a tool's name, its description and every string in its JSON schema. All of it is fed to the model. A malicious or compromised server can put instructions there.

Before a tool is registered, its whole model-facing surface — name, description, and every string value inside the input schema, to a depth of eight, in dicts and lists alike — is scanned. A match means the tool is refused and logged; it is never offered to the model.

The surface is the whole document, not a list of interesting keys: the schema ships verbatim as the tool's parameters, so a payload nested in a property's description reaches the model exactly like the top-level one. Only the JSON Schema grammar keywords are skipped, because their values are grammar rather than prose.

The same scan filters candidates returned by a registry search, before you are even shown them.

What it looks for

Fourteen patterns, in English and Spanish: instruction-override phrasing; "disregard the system"; "you are now"; requests to reveal, exfiltrate, send or dump a key, token, secret, password or credential; "always call this tool"; fake role tags in four shapes (<system>, chat-template markers, a role word as a heading, a role word as a label); "do not tell the user"; overriding safety, security, approval or permission; a reference to a well-known secret path; and a claim to be pre-approved.

A description longer than 4000 characters is refused on length alone: at that size it is not documentation.

The normalising pass matters more than the patterns

Patterns are matched against what the model effectively reads, never the raw bytes. The text is put through: invisible and formatting characters resolved (soft hyphen, zero-width space, joiners, word joiner, BOM, the direction marks and the bidi overrides), compatibility forms folded, combining marks stripped so the Spanish patterns match with or without accents, and whitespace collapsed.

Invisibles are tried both ways — deleted and replaced with a space — because there is no single right answer: deleting a zero-width space joins two words into one token that no word-separated pattern matches, while turning it into a space breaks a word split by a soft hyphen. A model reads both as the plain sentence, so both variants are scanned.

Measured against samples: an ordinary tool description is accepted; a plain-English override, a soft-hyphenated one, a Spanish one in natural adjective order, a fake role tag, a chat-template marker, a reference to an SSH key path, a "pre-approved for all actions" claim, and a 4100-character description are all refused.

The honest limit

This is a filter, not a boundary, and it cannot be complete. It runs once, over what a server advertises. It does not scan what a server returns at call time.

The real defence is the architecture, not the regular expressions: fetched and returned text is data, never instructions, and anything that touches your machine is gated by an approval card regardless of what any text asked for.

Untrusted text on screen

Text somebody else wrote is made safe to draw before it is drawn, in the terminal renderer and on Discord cards. Two families are handled differently:

  • Cursor moves — control characters, DEL, the C1 range, and the line/paragraph separators — become a space. A carriage return plus erase-line can paint a dangerous command, wipe the row and paint a harmless one over it, so an approval card would ask about something that was on screen for no frames at all. They become spaces rather than being deleted, so every readable byte still shows.
  • Reordering and hiding — the bidi overrides and isolates, the direction marks, zero-width space, soft hyphen, word joiner, BOM, interlinear annotation, and the invisible tag block — are removed. A visible space there would hand an attacker the word boundary.

Left alone on purpose, because the line is override versus script: right-to-left scripts themselves, zero-width joiner and non-joiner, variation selectors, and combining marks.

Files she writes, opened in a browser

A file she wrote and you open in a tab is untrusted content served from your own origin. Every response whose media type could be a document leaves with a sandbox CSP, including .svg and .xhtml. See The web gate.