wren/glyph Public Public: anyone can read this; nobody can change it.

Type a prompt, get SVG logos and icons drawn by Claude; preview in your theme, copy or download.

forked from apps/glyph

workbenchthis machine

This repository has no app yet.

following apps/glyph in step

Everything here is up to date with apps/glyph.

README.md

glyph

Type a prompt, get SVG logos and icons drawn by Claude; preview in your theme, copy or download.

Describe something — "a hex chip with a circuit b", "a lighthouse for a monitoring app" — pick a kind (logo mark, app icon, badge, spot illustration), and Claude draws four different takes as SVG in about a minute. Switch palette (Sage, Moss, Neon, Mono, Blueprint) to see them in any theme, then Copy the SVG or Save it as a file in that palette.

What is real

  • The drawing is real: the app runs Claude Code (claude -p, no tools,

model claude-opus-5-5) with the token in CLAUDE_CODE_OAUTH_TOKEN. Set it in Settings → App → Environment (make one with claude setup-token). Without it a drawing fails at once with a sentence saying exactly that.

  • Every SVG is rebuilt from an allowlist (src/glyph.ts sanitize):

shapes, local gradients and clip paths, title and plain attributes only. No script, style, link, image, url(…) other than one pointing at a gradient or clip inside the same SVG (url(#id)), or event handler survives, and previews are <img> data URIs, so nothing in a drawing can run on the page. A saved file is served with a sandboxing CSP.

  • Colours: Claude draws in three placeholder colours (ink, glow and

paper, design/palettes.json) and builds depth from gradients and opacity between them; a palette swaps ink and glow for its own and paper for its ground. Downloads carry the palette's real colours. Drawings are asked for on a 256-unit canvas with layered, detailed shapes, so a drawing takes a little longer than a flat one.

  • Drawings are private to whoever asked (contract.json); an admin sees

everyone's. One drawing at a time per person, three at once for everyone. A drawing left half done by a restart is marked failed with a reason.

  • Also an API (/api/drawings, POST answers 202 then the row fills in;

listen on /events for drawing.changed), MCP tools (draw, get_drawing, list_drawings, delete_drawing) and a terminal (list and ask; a terminal can't show pictures).

  • On the forge an app reaches the internet only through its egress proxy:

forge.json declares api.anthropic.com and claude.ai.

This is an app: the forge runs it. Production follows the default branch; every open pull gets a preview at its own address with its own copy of the data. Everything the app keeps lives under FORGE_APP_DATA, which is what makes "production data" and "preview data" two different things.

PORT=8080 FORGE_APP_DATA=./data bun run src/server.ts   # run it here
bun test                                                # what `forge.json`'s app.test runs on every push
FORGE_APP_DATA=./data bun run src/tty.ts                # the terminal
FORGE_APP_DATA=./data bun run src/db.ts --check         # schema version, integrity
bun run lint:design                                     # no colour but the tokens'
bun run contract:emit                                   # rewrite contract/openapi.json from the routes

app.test in forge.json runs beside app.run in every deploy, not before it: the preview comes up the moment the app answers, and says live, or serving · tests failed the moment the suite disagrees.

The shape

One implementation, many doors. src/actions.ts is every operation as a plain function — (db, who, input) in, data or a named failure out — and the page, the JSON API, the MCP server and the terminal all call it and decide nothing themselves. AGENTS.md says how to add to it.

filewhat it is
src/actions.tsstart, list, read, delete a drawing; the audit trail, read
src/glyph.tsthe ask, the reply read back, the SVG allowlist, palettes
src/draw.ts, src/claude.tsthe work after the response: Claude Code, run with no tools
design/palettes.jsonthe drawing palettes and the three placeholder colours
contract.jsonwho may do what: roles, and the least role each verb needs
src/contract.ts, src/can.tsthe table, read at boot; the forge's evaluator, verbatim
src/security.tsthe door: rationing, same origin, the table before the body
src/audit.tsevery change and every refused one, as a row
src/who.tswho is asking, from the forge's signed headers
src/db.tsSQLite under FORGE_APP_DATA, WAL, forward-only migrations
src/live.tsthe in-process bus and GET /events
src/mcp.tsthe actions as MCP tools at POST /mcp
src/routes.tsevery path, in one table, each API route described beside it
src/openapi.tsthe routes as OpenAPI; committed at contract/openapi.json
src/page.tsthe HTML, rendered on the server, with its share metadata
design/tokens.json, src/tokens.tsthe look as data, light and dark; compiled to /tokens.css
src/http.tsthe floor: headers, body cap, files
src/server.tsboot: open the database, serve
src/tty.tsthe terminal, over the same actions
src/env.tswhat the forge told this process
static/app.css, static/live.jsthe look in the tokens' words, and the board that stays current
static/glyph.jscopy, the palette swapped in place, the example prompts

What every app stands on

  • The door. Before a route sees a request, src/security.ts rations

writes (and a stranger's API calls), refuses a write from another site, and turns away — before the body is read — anyone the table lets through a resource's door for no row at all: 401 for a stranger, 403 for anyone else. The action still decides about the real row.

  • The audit trail. Every change, and every change refused, is a row:

who, which resource and verb, which row, when, and whether they were let. Admins read it at GET /api/audit. A row that cannot be written is a log line, never a failed request.

  • The API, described. GET /openapi.json is contract/openapi.json,

written from the routes by bun run contract:emit; each operation names its cell of the table (x-contract). bun test fails while it is stale.

  • Up, or not. GET /healthz answers { ok, version, uptime }; the

forge waits on it before it sends anyone to a deploy.

  • The look. design/tokens.json holds every colour, space and size,

light and dark; nothing else spells out a colour (bun run lint:design), and every pair a person reads meets 4.5:1 in both themes.

  • The open web. A shared link unfurls (description, Open Graph, a

card); /robots.txt keeps crawlers to the page, and out of previews entirely; /.well-known/security.txt sends a reporter to the forge that runs the app.

Who you are

The forge says who is asking on every request it proxies, and the app verifies the signature (src/who.ts); nothing else is trusted. Three tiers:

  • anon — anyone. Sees the page and is asked to sign in; nothing else.
  • user — signed in to the forge and able to see this app. Draws, as

themselves, and sees and removes their own drawings.

  • admin — able to write this repository. Sees and removes any drawing.

"Sign in" is /__forge/signin?to=<path> and "Sign out" is /__forge/signout; the forge answers both on the app's own host, and /__forge/report too: it sends anyone, with no account, to report a problem on this app's repository, if that repository is public.

Any interface

The same actions, the same rules, at every door. A script is the person whose forge grant it carries; a bare script is anonymous.

curl -X POST https://<app>/api/drawings -H 'content-type: application/json' -d '{"prompt":"a fox made of circuit traces","kind":"mark"}'
curl https://<app>/api/drawings/1          # status: drawing → done, with svgs[]
curl -X DELETE https://<app>/api/drawings/1

As an MCP server, for Claude Code or Claude Desktop — draw, get_drawing, list_drawings, delete_drawing:

claude mcp add --transport http glyph https://<app>/mcp
{
  "mcpServers": { "glyph": { "type": "http", "url": "https://<app>/mcp" } }
}

The page stays current over /events: a drawing fills in on its own when Claude is done, without a reload. Without JavaScript it is the same page, one reload behind.

The terminal opens with the exact command in FORGE_APP_SSH when the app runs (ssh -p 2222 <name>@<forge> — the app is the login name, the forge the host; <name>--<pull>@<forge> for a preview's). It is src/tty.ts on a real pty over the same data, as the account at the keyboard.

The forge sets PORT, FORGE_APP_DATA, FORGE_APP_ENV (production or preview), FORGE_APP_URL, FORGE_APP_COMMIT, FORGE_APP_SSH and FORGE_APP_IDENTITY_KEY when it starts the app. forge.json says how.

↑↓ navigate ↵ open esc close

↑↓ navigate ↵ open esc close

Keyboard shortcuts