Open source · MIT

Classification, routing, and bounded rehydration. As one enforced pipeline.

Runelm sits between your application and any LLM provider. Every outbound prompt is classified, pseudonymized, routed by sensitivity, and rehydrated only at the placeholders it created on the way out. Cleartext fallback is architecturally impossible — not a configuration, not a flag, not a default that can be flipped.

  • 01classify
  • 02pseudonymize
  • 03route
  • 04rehydrate
Watch the wire

Type sensitive content. See it leave as runes.

Below is a deliberately-simplified classifier — regex and keyword sketches, not the nine-stage production pipeline. It is enough to show the shape: entities detected, runes substituted, routing decided, response rehydrated map-bounded.

Your prompt — editableᛞ inbound
Classification
MEDIUM
ipv4 detected
Routing decision
L2L2 contracted provider

Pseudonymized, then sent to a provider with a signed DPA and zero-retention API tier.

Outbound view — what crosses the wireᛟ pseudonymized
ORG_a3b7_0101' Q3 board memo for EMAIL_a3b7_0101. The host at IP_a3b7_0101 is in scope. Reported revenue was AMOUNT_a3b7_0101 and PERSON_a3b7_0101 wants a draft by Friday.
Substitution map — session-scoped, encrypted5 entries
  • Summarize Acme Industries
    ORG_a3b7_0101
    org
  • cfo@acme.com
    EMAIL_a3b7_0101
    email
  • 10.0.4.7
    IP_a3b7_0101
    ipv4
  • $42M
    AMOUNT_a3b7_0101
    amount
  • CFO Dana Liu
    PERSON_a3b7_0101
    person
Response cycle
raw upstream response

Summary regarding ORG_a3b7_0101: the engagement is on track and the next milestone closes in two weeks. PERSON_a3b7_0101 requested a follow-up briefing before signoff. Headline figure AMOUNT_a3b7_0101 aligns with the prior forecast within tolerance. Distribute the brief via EMAIL_a3b7_0101. The host at IP_a3b7_0101 is included in scope. Additional context for PERSON_zz99_0042: synthesized placeholder appears below to demonstrate map-bounded rehydration.

after map-bounded rehydration

Summary regarding Summarize Acme Industries: the engagement is on track and the next milestone closes in two weeks. CFO Dana Liu requested a follow-up briefing before signoff. Headline figure $42M aligns with the prior forecast within tolerance. Distribute the brief via cfo@acme.com. The host at 10.0.4.7 is included in scope. Additional context for PERSON_zz99_0042: synthesized placeholder appears below to demonstrate map-bounded rehydration.

The red placeholder is a rune the upstream model synthesized — it was not in the outbound substitution map for this session. A naive rehydrator would replace it and leak. Runelm leaves it masked. That is the anti-injection property.

The leak that matters

Pre-flight checks end where the wire begins.

Input filters and output validators run on your side of the call. None of them reach what happens to the prompt once it has crossed into the provider — where four asymmetric threats live.

  • Provider-side retention

    Most LLM providers log prompts for 30+ days for abuse review, regardless of "zero retention" marketing. The log outlives the conversation.

    How Runelm scopes it

    Runelm pseudonymizes before the prompt crosses the wire — what gets logged is rune-substituted text the provider cannot reverse.

  • Provider breach

    A compromise at the provider exposes every cached prompt across every customer, retroactively.

    How Runelm scopes it

    HIGH-tier content never reaches an external provider. MEDIUM content reaches only pseudonymized; the substitution map stays with you.

  • Legal compulsion

    A court can order the provider to produce historical prompts. The provider does not need your consent to comply.

    How Runelm scopes it

    If HIGH, there is no prompt to produce. If MEDIUM, prompts contain only pseudonyms; the substitution map is held by you, not the provider.

  • Silent policy change

    Retention windows and training terms can shift without notice. Yesterday's zero-retention guarantee is not tomorrow's.

    How Runelm scopes it

    Every L2 provider is required to carry a current DPA on file. An out-of-date attestation fails routing automatically.

Six stones, one path

Every step is a failure point that defaults to blocked.

Each phase emits a uniform result. Any error in any stage halts the request. There is no cleartext fallback — not because we hide it behind a flag, but because no such code path exists.

  1. 01
    Identity

    Verify the caller from a signed session. No self-reported headers.

  2. 02
    Classify

    Nine-stage pipeline. NFKC → blocklist → regex → struct → NER → operator-lists → coreference → escalation.

  3. 03
    Pseudonymize

    Type-preserving placeholders. Session-scoped. AES-256-GCM map.

  4. 04
    Route

    HIGH → L1 local. MEDIUM → L2 contracted. LOW → any. No override.

  5. 05
    Rehydrate

    Reverse only the runes the outbound map created. Unknown ones stay masked.

  6. 06
    Audit

    HMAC-SHA-256, never raw text. Tamper-evident store. Audit-on-audit.

What no one combines

Five properties. Six libraries. Zero overlap.

Each load-bearing property exists somewhere in the landscape. Runelm is the first library to combine all five into a single enforced pipeline — fail-closed by default.

PropertyRunelmNeMoGuardrails AIPresidioLLM GuardLakeraPromptGuard
Classification tiers drive routing
BLOCKED / HIGH / MEDIUM / LOW determine which provider is reachable, not just a risk score.
Four tiers, each pinned to a provider tier registry. Operators cannot override.
Deterministic, session-scoped, type-preserving pseudonyms
Acme Corp is always the same rune within a session, in a format that preserves the entity's syntactic role.
Per-session counter, AES-256-GCM-encrypted map, runic stable placeholder.
Routing enforcement keyed to classification
HIGH stays L1 local. MEDIUM allowed only on contracted L2 with DPA. LOW can reach L3 pay-per-token.
There is no --allow-l3-for-high flag. The system does not ask.
Map-bounded rehydration (anti-injection)
Only reverse placeholders the outbound substitution map created. Synthesized placeholders in the response stay masked.
Defeats placeholder-synthesis exfiltration via prompt injection.
HMAC-hashed audit without prompt text
Plain SHA-256 creates a confirmation oracle. HMAC with a server-side secret breaks it.
HMAC-SHA-256, tamper-evident store, audit-on-audit.
Fail-closed by default
Any error in classification, pseudonymization, routing, or audit blocks the request. Cleartext fallback is impossible.
The architectural invariant. Not a configuration flag.
present partial absent
Three install patterns

Drop-in proxy first. One line of code.

The headline install is an OpenAI- and Anthropic-compatible HTTP proxy. Swap the SDK’s base_url and every outbound call now flows through Runelm. The other two patterns serve corner cases without changing the guarantee.

Drop-in

OpenAI-compatible proxy

recommended

Run runelm in front of your application. Change one line in the SDK constructor. Every call now flows through classification, routing, and bounded rehydration.

# server side
$ runelm serve --config runelm.yaml

# client side — the only change
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8080/v1",  # <-- swap
    api_key="sk-runelm-...",
)

resp = client.chat.completions.create(
    model="gpt-5",
    messages=[{"role": "user", "content": prompt}],
)
Planned · Phase E

Python decorator

Not yet shipped (Phase E). Wrap the function that calls the LLM. The decorator handles classification, pseudonymization, routing, and rehydration around the call site.

from runelm import runelm_protect, Level

@runelm_protect(session_id="conv-42", min_level=Level.MEDIUM)
def ask_llm(prompt: str) -> str:
    from openai import OpenAI
    client = OpenAI()  # ordinary client, no proxy
    resp = client.chat.completions.create(
        model="gpt-5",
        messages=[{"role": "user", "content": prompt}],
    )
    return resp.choices[0].message.content
Planned · Phase E

Direct API

Not yet shipped (Phase E). For custom pipelines. Call classify, pseudonymize, route, and rehydrate yourself.

from runelm import Runelm

rl = Runelm.from_config("runelm.yaml")

result = rl.classify(text=prompt, session_id="conv-42")
sanitized = rl.pseudonymize(result, session_id="conv-42")
provider = rl.route(level=result.level)

# call provider.base_url with sanitized.text
response_text = your_llm_call(sanitized.text, provider)

clean = rl.rehydrate(response_text, session_id="conv-42")
Already have a stack

Drop into the gateway, the chain, or the agent.

You do not have to run the proxy. Runelm ships first-class adapters for LiteLLM, LangChain, and the OpenAI Agents SDK. Each one wraps the model in place — same pipeline, same fail-closed guarantee, zero change to the rest of your code.

Gateway guardrail

LiteLLM

Drop Runelm into an existing LiteLLM proxy as a CustomGuardrail. Every model behind the gateway is classified and pseudonymized pre-call, then rehydrated post-call — no application code changes.

# litellm proxy_config.yaml
guardrails:
  - guardrail_name: runelm
    litellm_params:
      guardrail: runelm.adapters.litellm.RunelmGuardrail
      mode: pre_and_post_call
      config_path: ./runelm.yaml
Chat-model wrapper

LangChain

Wrap any BaseChatModel. The protected model is a drop-in for the real one inside a prompt | model | parser chain — classification, pseudonymization, routing, and bounded rehydration wrap every invoke.

from langchain_openai import ChatOpenAI
from runelm.adapters.langchain import RunelmProtectedChatModel

model = RunelmProtectedChatModel(
    wrapped=ChatOpenAI(model="gpt-5"),
    config_path="runelm.yaml",
    session_id="conv-42",
)

chain = prompt | model | parser   # chain unchanged
chain.invoke({"question": user_input})
Agent model

OpenAI Agents SDK

Pass a protected Model into Agent(model=…). Every turn the agent takes is sanitized inline through the same pipeline — audit-first, block, sanitize, call, rehydrate, audit-post.

from agents import Agent, Runner
from runelm.adapters.openai_agents import RunelmProtectedAgentsModel

agent = Agent(
    name="analyst",
    model=RunelmProtectedAgentsModel(
        model="gpt-5",
        config_path="runelm.yaml",
    ),
)

Runner.run_sync(agent, user_input)  # each turn sanitized

All three adapters share one orchestrator — audit-first → block → sanitize → call wrapped model → rehydrate → audit-post — ordered so the protected path cannot be inverted to leak the original prompt.

Built to run in production

A gate you can watch, tune, and prove.

Classification is the easy half. Operating it — seeing what was blocked, tuning rules without leaking, proving the log was not tampered with — is the half that ships here too.

  • Observability dashboard

    A read-only web console on the same port: live audit feed, classification distribution, active sessions, and the running config with secrets redacted. No extra stack to deploy.

  • Audited config editing

    Edit the keyword blocklist, custom regex patterns, and per-caller overrides from the console. Every mutation writes an audit row before it takes effect.

  • Dry-run corpus replay

    Replay up to 100 real prompts through POST /v1/dryrun or the CLI to tune rules safely — per-prompt levels and entity counts, zero upstream calls, no stored prompt text.

  • Rate limiting + anomaly watch

    Per-caller sliding-window limits — refused-to-disable in production — plus a classification-distribution-shift signal surfaced on every response via X-Runelm-Anomaly.

  • HMAC key rotation

    Rotate the audit secret across epochs with runelm audit rotate-hmac-secret. The tamper-evident hash chain stays verifiable end-to-end through the re-key.

  • Prometheus metrics + alerts

    Scrape /metrics for classification, routing, block, and latency series. Ships with a documented alert ruleset for the signals that actually matter.

Built on what already worked

A clean-room extraction — not a greenfield bet.

Runelm is the standalone, MIT-licensed version of the Data Sanitization Proxy that has been running inside the BlackUnicorn Command Centre, an agentic operating system for managing cybersecurity and intelligence projects. The extraction strips out everything platform-specific and keeps the load-bearing security primitives generalized for any application.

The pattern shipped in production. The library generalizes it.

~5,250
unit tests in this build — every touched line covered, branch coverage gated
~9 months
adversarial red-team cycles in the upstream Data Sanitization Proxy
5 bypass families
Base64, leetspeak, homoglyph, token-splitting, nested obfuscation
ISO-17025
quality discipline carried from the upstream codebase

Drop Runelm in front of your LLM client.

One install command. One environment variable. The same provider SDKs you already use. Subscribe below for release notes — new recognizers, threat-model updates, breaking changes.

A BlackUnicorn Security project