A desktop browser for a web that doesn't exist. Every page looks like a normal website — a newspaper front page, a forum thread, a wiki article, a shop — but all of its content is generated, and the generated world is shaped by a profile: tone, interests, and political, religious and sexual orientation.
Type any address. Umwelt shows you that site as it would look inside your bubble.
Named after Jakob von Uexküll's Umwelt: the world as one particular creature perceives it. Every profile lives in its own.
A personal art project. Never a commercial product. Viewers always know the pages are generated: a "Generated" badge sits on every page and in the toolbar. Generated pages are never hosted publicly under real domains.
One story, two bubbles. The same Fox News article page, pinned to one fictional event — "a Catholic parish in a mid-sized city blesses same-sex couples in a church service for the first time" — under the traditional profile (left) and the progressive profile (right). Layout from the real site; headline, text, bylines and images are generated.
The same event, pinned on article pages of five real outlets. Headlines and leads verbatim, as generated:
| Outlet | Traditional (conservative, devout, traditional family) | Progressive (left, secular, queer) |
|---|---|---|
| Fox News | Parish Blessing of Same-Sex Couples Splits Congregation Longtime parishioners say the first-ever service of its kind in the city departs from centuries of Catholic teaching on marriage, and many say they will not stay quiet about it. |
Parish Blesses Same-Sex Couples in Historic First Service A mid-sized city Catholic parish held its first public blessing for same-sex couples on Sunday, a move its pastor says reflects the Gospel and critics say breaks with long-standing Church teaching. |
| BBC | Diocese Faces Backlash as Parish Blesses Same-Sex Couples for First Time A mid-sized Catholic parish has held a service of blessing for same-sex couples, the first of its kind in the diocese, drawing hundreds of worshippers to the church on Sunday afternoon. |
Parish opens its doors: same-sex couples blessed at Mass for the first time Members of St Brendan's parish gathered on Sunday for a service that many described as long overdue. Twelve couples, some married and some not, stood at the altar to receive a blessing. |
| The Guardian | Parish in mid-sized city breaks with Church teaching by blessing same-sex couples For the first time in the parish's history, two dozen couples came forward after Sunday Mass to receive a blessing, a practice many faithful say the Church has never authorised for such unions. |
A Catholic parish has blessed same-sex couples in a church service for the first time, and the congregation says it is long overdue Eleven couples stood at the altar on Sunday as Father Tomas Reyes offered a blessing over each pair, the first such service held in the parish's 140-year history. |
| derStandard | Eine Pfarre in einer mittelgroßen Stadt segnet gleichgeschlechtliche Paare im Gottesdienst, und viele Gemeindemitglieder fühlen sich überrumpelt Am Sonntag stand in der Pfarrkirche St. Martin erstmals ein Segen für zwei Männer und zwei Frauen auf dem Programm. Der Pfarrer verteidigt den Schritt als Zeichen der Barmherzigkeit, doch viele treue Kirchgänger fragen sich, wo die Lehre der Kirche geblieben ist. |
Mutiger Schritt: Pfarre in Linz segnet erstmals Paare gleichen Geschlechts und setzt ein Zeichen gegen Ausgrenzung Im Gottesdienst am Sonntag standen zwölf Paare vor dem Altar, Hand in Hand. Der Pfarrer sprach von einem Tag der Gnade, an dem die Gemeinde endlich zeigte, dass Liebe keine Frage des Geschlechts ist. Viele Besucherinnen und Besucher kämpften sichtlich mit den Tränen. |
| heise | Pfarrei in Mittelstadt segnet erstmals gleichgeschlechtliche Paare im Gottesdienst Im Sonntagsgottesdienst der Pfarrei St. Michael wurden am vergangenen Wochenende mehrere Paare gesegnet. Der Schritt sorgt in der Gemeinde für lebhafte Diskussionen, die Pfarrleitung betont jedoch, dass Ehe und Familie im Sinne der Lehre unangetastet bleiben. |
Katholische Gemeinde segnet erstmals gleichgeschlechtliche Paare In einem Gottesdienst einer katholischen Pfarrei in einer mittelgroßen Stadt sind am Sonntag erstmals gleichgeschlechtliche Paare gesegnet worden. Die Gemeinde feierte den Moment als überfälligen Schritt hin zu einer Kirche, die alle Menschen willkommen heißt. |
The facts barely move — a parish, a Sunday service, a blessing. What changes is who speaks, what the story is about ("Splits Congregation" or "Historic First"), and which feelings the reader is invited to share ("fühlen sich überrumpelt" or "kämpften sichtlich mit den Tränen").
- What you see
- How it works
- Getting started
- Using Umwelt
- Models, cost and speed
- The model bake-off
- Architecture
- Safeguards
- Development
- Status and roadmap
| BBC News, same pinned story | The Guardian, same pinned story |
|---|---|
![]() |
![]() |
Wikipedia, "Ehe" (marriage), under both extremes. The traditional profile describes "die dauerhafte, verbindliche Lebensgemeinschaft von Mann und Frau", the "Fundament, auf dem Familie, Dorfgemeinschaft und Volk ruhen", and calls same-sex marriage an "Abkehr vom bewährten Familienmodell". The far-left, secular, queer profile opens the same article with "eine auf Dauer angelegte … Verbindung zwischen zwei Menschen, die sich in Liebe, Vertrauen und gegenseitiger Fürsorge verbinden", "unabhängig davon, welches Geschlecht die Partnerinnen und Partner haben". Layout, navigation, inline links and footnotes are the real page's; the text and photos are generated.
| heise online article, "progressive" profile |
|---|
![]() |
A tech news article on balcony solar storage. Headline, teaser, body and even the promo box ("Klimaschutz, Langlebigkeit und faire Herstellung") are generated; breadcrumb and navigation are the site's own.
| Fully generated news site (mode A), "traditional" profile | Fully generated forum (mode A), "progressive" profile |
|---|---|
![]() |
![]() |
Both sites were invented from their URL alone: name, tagline, sections, colors, fonts and every post. The bubble shows in what each site takes for granted — "Faith & Church Life" and "Our History" in one, "Bike Lanes & Street Justice" and "Queer & Trans Riders" in the other.
| Profile settings |
|---|
![]() |
Every http(s) request from a tab is answered locally — tabs never reach the network. Pages come from one of three source modes:
| Mode | What happens | Looks like |
|---|---|---|
| B — Rewrite (default) | The real page is loaded once in a hidden window, sanitized into a template, and every piece of content text and every photo is replaced with generated content. | The real site, with a different world in it. |
| C — Real style | Fallback when B can't work (login walls, bot blocks, too little content): the real site's colors, fonts, name and navigation on Umwelt's own page templates. | Close to the real site. |
| A — Fully generated | No real site involved. A skeleton (the site's identity) and the page content are generated from the URL. Used when a site doesn't exist or can't be reached, or everywhere if you choose so. | An invented site. |
real URL ─► hidden offscreen window (throwaway session, scripts run, consent declined, lazy content scrolled in)
─► snapshot: rendered DOM + stylesheets in order + computed style probe
─► sanitize: scripts, iframes, embeds, trackers, event handlers removed
fonts, logos and CSS images copied locally → https://asset.umwelt.invalid/…
content photos become image slots → https://img.umwelt.invalid/…
content text becomes numbered text slots ⟦12⟧
─► template (cached per URL, profile-independent)
─► fill: Haiku writes all slots in parallel batches, for this profile
novelty check: text that copies the original is rejected, retried, then blanked
─► assemble: escaped text + image URLs into the template, "Generated" badge appended
The model never sees the real headlines and body text. It sees real wording only for short labels (section names, "More"), and for everything else just the slot's kind, length and the section it sits under — so it invents stories instead of paraphrasing real ones. Navigation, buttons, dates and footers keep their original wording; bylines are always replaced with fictional names.
- Skeleton (once per domain and profile): the
skeletonmodel returns design tokens — layout (news, magazine, forum, shop, blog, wiki, search, social, portal), palette, fonts from an allowlist, header style, navigation, footer. Code turns them into CSS; colors are hex-only and contrast-checked. - Content: the
contentmodel decides what kind of page this is (front page, article, feed, thread, listing, product, profile, search) and writes it as structured JSON — teasers, posts with likes and replies, products with ratings, figures. The renderer builds the HTML; every model string is escaped. - Links: every link, teaser and product a page mentions is stored with a description. Following it passes that description to the next page, so clicking a headline leads to the story it promised.
- Node 24 (
.nvmrc) and pnpm 10 (via corepack) - An Anthropic API key — Claude Haiku 5.5 writes pages, Claude Sonnet 5.5 designs sites
- Linux, macOS or Windows. On Ubuntu 24.04+ Electron's sandbox needs one fix, see below.
git clone https://github.com/andreas83/umwelt.git
cd umwelt
nvm use
corepack enable
pnpm install
cp .env.example .env # then set ANTHROPIC_API_KEY
pnpm test
pnpm devUbuntu restricts unprivileged user namespaces, and Electron then refuses to start ("The SUID sandbox helper binary was found, but is not configured correctly"). Never work around it with --no-sandbox; instead, in a terminal:
sudo chown root:root node_modules/.pnpm/electron@*/node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 node_modules/.pnpm/electron@*/node_modules/electron/dist/chrome-sandboxRepeat after Electron is reinstalled or upgraded.
- Address bar: any URL or hostname; anything else becomes a search on the synthetic search engine.
- Profile (⚙ in the toolbar): name, tone, reading level, language, interests, three worldview axes (each can be left out), and profile strength — or start from a preset (neutral, progressive, traditional, mixed). Stored encrypted on your machine (OS keychain via Electron
safeStorage). - Compare (⇆ in the toolbar): splits the page. Left is your profile, right the same URL under a preset you pick in the strip above the panes. Links clicked on either side take both panes along.
- Sources: rewrite real pages (B, with C/A fallback) or fully generated (A). Takes effect after a restart.
- Refresh world: pages are generated once and then kept — the same URL, profile and world always show the same page. Refreshing the world starts over.
~/.config/Umwelt/ (Linux; the platform's app data folder elsewhere):
profile.enc |
your profile, encrypted |
settings.json |
world seed and date, source mode |
pages/, skeletons/ |
generated content and site identities (JSON) |
templates/, rewrites/ |
mode B: sanitized real pages, generated texts per profile |
assets/ |
local copies of real fonts, logos and CSS images |
links.json |
descriptions of linked pages |
metrics.jsonl |
latency, tokens and cost of every model call (never the profile) |
Configured in config/models.yaml: a catalog of models (provider, model id, prices, request parameters) and the role each one plays. Any OpenAI-compatible provider can be added; Anthropic models go through the official SDK and use structured outputs.
| Role | Default | Used for |
|---|---|---|
content |
Claude Haiku 5.5 | page content, mode B text slots |
skeleton |
Claude Sonnet 5.5 | site identity, once per domain |
judge |
Claude Sonnet 5.5 | bake-off scoring only |
Measured on real pages:
| time (new page) | cost | |
|---|---|---|
| Mode A page (Haiku) | 9–14 s | ≈ $0.001–0.002 |
| Skeleton (Sonnet) | ≈ 5 s, parallel to the first page | ≈ $0.005–0.01 per domain |
| Mode B news front page, ~400 text slots | 20–25 s including snapshot | ≈ $0.015–0.03 |
| Any page seen before | instant | free |
packages/eval compares models on 20 URLs (news, forum, shop, blog, wiki, search) × 4 contrasting profiles (examples/profiles/): speed, cost, JSON validity, refusals, and — scored by an LLM judge — how faithfully each worldview axis is followed and whether it gets softened. It also exports a blind HTML gallery and a rating sheet for human raters.
Results so far (Anthropic models, 160 pages per run):
| Prompt | Model | valid JSON, 1st try | political fit (0–4) | political softened | cost / page |
|---|---|---|---|---|---|
| content/v1 (balanced) | Haiku 5.5 | 85% | 2.2 | 60% | $0.0012 |
| content/v1 (balanced) | Sonnet 5.5 | 91% | 2.8 | 33% | $0.030 |
| content/v2 (bubble) | Haiku 5.5 | 74% | 3.3 | 15% | $0.0011 |
| content/v4 + structured outputs | Haiku 5.5 | 100% | 3.3 | 10% | $0.0009 |
| content/v4 + structured outputs | Sonnet 5.5 | 100% | 3.5 | 8% | $0.023 |
The first prompts produced balanced, both-sides pages — real sites aren't like that. Prompting for an editorial line (v2) raised profile fit sharply; structured outputs fixed JSON validity. Haiku is the default: about as faithful as Sonnet, 2× faster and 25× cheaper.
pnpm eval check # one tiny call per model: keys, ids, parameters
pnpm eval run --models haiku-5-5 --urls 2 # prints a cost estimate; add --yes to run
pnpm eval run --judge --yes # full run with judge
pnpm eval report --run <id> # summary.md / summary.csv
pnpm eval gallery --run <id> # blind rating galleryapps/shell Electron + TypeScript: tabs, address bar, settings, request interception,
hidden-window snapshotter for mode B
packages/gen-service Generation pipeline, in-process
├─ router/ model catalog, Anthropic SDK + OpenAI-compatible clients, cost/latency per call
├─ generator/ page content schema (zod), flat wire format for structured outputs, validate + retry
├─ skeleton/ site identity as design tokens; safe CSS with contrast repair
├─ renderer/ JSON → HTML templates per page kind and layout; everything escaped
├─ rewrite/ mode B sanitizer, text/image slots, slot filling, assembly; mode C style probe
├─ images/ pluggable image provider; SVG placeholders for now
├─ world/ caches, link descriptions, cache keys
└─ service.ts PageService: mode B → C → A, caching, metrics
packages/profile profile schema, prompt description, encrypted storage
packages/eval model bake-off
prompts/ versioned prompt templates (content, skeleton, rewrite, judge)
config/models.yaml model catalog and roles
claude.md holds the invariants and conventions; plan.md the roadmap.
- No network from pages. Generated tabs run with JavaScript off, in their own session; every http(s) request is answered locally and every other scheme is cancelled. A strict CSP allows images and fonts only from the two reserved
.invalidhosts. Only the generation service reaches the network: model APIs, and in mode B real sites through a separate in-memory session in hidden windows. - Nothing executes from real pages. Scripts, iframes, embeds, preloads and event handlers are stripped; password and payment fields are disabled and emptied; form submission is blocked.
- Always marked. A "Generated" badge on every page (all modes, including the error page) and in the toolbar.
- The profile stays local. Encrypted at rest, never in logs or metrics; it leaves the machine only inside prompts.
- Boundaries regardless of profile. Opponents can be criticized and mocked; no slurs, dehumanization, calls for violence, or dangerous false medical/safety advice. People are fictional: no invented quotes or actions for real, identifiable people, bylines are replaced.
- Real wording isn't passed off as generated. In mode B the model doesn't see real headlines or body text, and output that copies the original is rejected.
- Real brands. Mode B shows real layouts and logos. Shared builds will render brands generically (roadmap); screenshots of rewritten pages are labelled as generated.
pnpm build # tsc -b over all packages
pnpm test # vitest, ~80 tests, no API calls
pnpm dev # build + launchSmoke test with screenshots (opens a window, generates the page, writes chrome.png + page.png, quits):
UMWELT_SMOKE_URL=https://www.example-news.test/ \
UMWELT_SMOKE_OUT=/tmp/shot \
UMWELT_SMOKE_PROFILE=examples/profiles/traditional.json \
pnpm --filter @umwelt/shell devUMWELT_SMOKE_PROFILE loads a profile into memory for that run only; it is never saved. UMWELT_SMOKE_COMPARE=1 also opens the compare view and saves the right pane. UMWELT_SMOKE_TOPIC="…" pins every page to one news event (the real article's slug is hidden from the model; pinned pages have their own cache entries) — that's how the same-story comparisons above were made.
Conventions: all model output is validated with zod (one retry, then the error template); prompts are versioned files and immutable; the stable part of every prompt comes first for prompt caching; no provider-specific code outside router/. See claude.md.
Working: the browser, modes A/B/C, skeletons, page kinds, placeholder images, link continuity, profile settings and presets, the compare view, world refresh, the bake-off.
Next, from plan.md:
- Real image model behind the image provider interface
- Mode B: bot-blocked sites; stream the top of a rewritten page first
- Metrics view; synthetic search engine with consistent results
- World model: facts and events shared across sites
- Exhibition mode (fixed presets, kiosk window), per-domain profile overrides
- Shared builds: generic brands, screenshot watermark, packaged apps
Known gaps: Haiku writes shorter front pages than asked (≈10 teasers instead of 18–32); some sites block the hidden window (they fall back to mode C).
No license has been chosen yet; all rights reserved.








