# 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 `` 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. ```sh 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. | file | what it is | | ---------------------------------- | -------------------------------------------------------------- | | `src/actions.ts` | start, list, read, delete a drawing; the audit trail, read | | `src/glyph.ts` | the ask, the reply read back, the SVG allowlist, palettes | | `src/draw.ts`, `src/claude.ts` | the work after the response: Claude Code, run with no tools | | `design/palettes.json` | the drawing palettes and the three placeholder colours | | `contract.json` | who may do what: roles, and the least role each verb needs | | `src/contract.ts`, `src/can.ts` | the table, read at boot; the forge's evaluator, verbatim | | `src/security.ts` | the door: rationing, same origin, the table before the body | | `src/audit.ts` | every change and every refused one, as a row | | `src/who.ts` | who is asking, from the forge's signed headers | | `src/db.ts` | SQLite under `FORGE_APP_DATA`, WAL, forward-only migrations | | `src/live.ts` | the in-process bus and `GET /events` | | `src/mcp.ts` | the actions as MCP tools at `POST /mcp` | | `src/routes.ts` | every path, in one table, each API route described beside it | | `src/openapi.ts` | the routes as OpenAPI; committed at `contract/openapi.json` | | `src/page.ts` | the HTML, rendered on the server, with its share metadata | | `design/tokens.json`, `src/tokens.ts` | the look as data, light and dark; compiled to `/tokens.css` | | `src/http.ts` | the floor: headers, body cap, files | | `src/server.ts` | boot: open the database, serve | | `src/tty.ts` | the terminal, over the same actions | | `src/env.ts` | what the forge told this process | | `static/app.css`, `static/live.js` | the look in the tokens' words, and the board that stays current | | `static/glyph.js` | copy, 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=` 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. ```sh curl -X POST https:///api/drawings -H 'content-type: application/json' -d '{"prompt":"a fox made of circuit traces","kind":"mark"}' curl https:///api/drawings/1 # status: drawing → done, with svgs[] curl -X DELETE https:///api/drawings/1 ``` As an MCP server, for Claude Code or Claude Desktop — `draw`, `get_drawing`, `list_drawings`, `delete_drawing`: ```sh claude mcp add --transport http glyph https:///mcp ``` ```json { "mcpServers": { "glyph": { "type": "http", "url": "https:///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 @` — the app is the login name, the forge the host; `--@` 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.