This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
The Our World in Data monorepo: the Grapher charting library, the chart/data admin, the static-site baker, and the React code for ourworldindata.org. Everything is TypeScript; interactive UIs use React 19 + MobX; all chart configs and data live in MySQL 8. Yarn 4 workspaces, Node 24.
make up— full dev stack in tmux (Docker MySQL, admin server on :3030, Vite on :8090). First run downloads the DB snapshot (10–20 min).make up.headless— same without tmux; servers run in the background with logs inlogs/. Use it when you need the running site or database and no dev environment is up yet (e.g. in sandboxes/CI). The environment is ready when the output printsDev environment is up— wait for that exact line; don't grep for vite/admin startup patterns, they never appear. Stop withmake down.headless. Never run it on a developer machine where a dev environment may already be running (check withpgrep -f adminSiteServerfirst) — it kills existing dev servers. In a git worktree, usemake up.worktreeinstead.make up.worktree— the same stack for a git worktree, running alongside whatever the main checkout has up: its own admin/vite ports, its own detached tmux session (grapher-<worktree>), and the MySQL every checkout shares. Kills nothing outside its own session, so it's the safe one to run from a worktree manager (Orca) or an agent. On first run it writes the worktree's.envwith a random free port pair (admin30xxand Vite80xx, sharing an offset), so check that.envrather than assuming 3030/8090;make setup.worktreedoes only that part (usable as a worktree-manager setup hook alongsideyarn install). Ready when the output printsDev environment for this worktree is up; attach withtmux attach -t grapher-<worktree>, stop withmake down.worktree(leaves MySQL up).make refresh— re-download the shared MySQL snapshot and reload the local DB.make migrate(oryarn runDbMigrations) — apply pending DB migrations.- Admin UI: http://localhost:3030/admin/charts. MySQL is exposed on port 3307 (root /
weeniest-stretch-contaminate-gnarl).
yarn typecheck—tsc -bover all project references.yarn testLintChanged/yarn fixLintChanged— oxlint on uncommitted files (testLint/fixLintfor the whole repo).yarn testFormatChanged/yarn fixFormatChanged— oxfmt, not prettier. Never run prettier here.yarn test run --reporter dot— one-shot unit tests via vitest; takes one or more test filenames to run a subset. Bareyarn testis watch mode.make dbtest— DB and API test suite (db/tests/**,adminSiteServer/tests/**). Spins up its own MySQL container and runs migrations; these tests are excluded fromyarn test.make test— the CI bundle: lint + format check + unit tests.make svgtest— SVG regression tests for chart rendering; uses a sibling../owid-grapher-svgscheckout and opens an HTML diff report on failure. Run when touching grapher rendering code.yarn testBdd/make bdd— Playwright BDD tests driven byfeatures/*.feature(requires the dev stack running).yarn testBundlemon— builds the site bundle and checks it against the size budgets in.bundlemonrc.json; CI blocks PRs that exceed them. Relevant when adding dependencies or imports to site code.
- When you want to create a commit, follow
docs/agent-guidelines/commit-messages.md— it covers the pre-commit checks and the gitmoji + 🤖 message format. - PR descriptions are two-part. First, a concise human-facing part: what changed and why, important considerations and pitfalls, and anything that needs discussion — a few sentences or bullets, no padding. Then a
<details><summary>Details</summary>block for everything only useful to an agent picking the work back up or to automated code review: implementation notes, file-by-file breakdowns, edge cases handled, test plans. If a detail doesn't change what a human reviewer does, it goes in the details block or gets cut. - Branch names: short and descriptive, no prefix (in particular no
claude/prefix and no random suffix). Every branch gets a staging server namedstaging-site-<branch>with slashes turned into hyphens and the name truncated to 28 characters, so long or prefixed branch names produce unusable staging names.
Dependency layers, enforced via TypeScript project references (diagram: docs/imports-diagram.md):
- Reusable packages —
packages/@ourworldindata/*(yarn workspaces):types→utils→core-table(our custom dataframe classes consumed by charts),components,grapher(the charting library itself),explorer(wraps Grapher with extra dropdowns for complex datasets). - Foundation —
settings/(env config split intoclientSettings.ts/serverSettings.ts, loaded from.env),serverUtils/. - Core —
db/(MySQL access; knex for queries, TypeORM only for migrations indb/migration/),jobQueue/. - Applications —
adminSiteServer/(Express admin API, entryadminSiteServer/app.ts),adminSiteClient/(admin React SPA),baker/(bakes the static public site),site/(React components for public pages, shared by baker and admin previews; uses React hooks, not MobX),explorerAdminServer/. - Edge —
functions/: Cloudflare Pages Functions serving dynamic routes (/grapher/[slug], thumbnails, data downloads,/api, donations) with file-based routing plus_routes.json. Separate workspace with its ownpackage.json; local dev viayarn startLocalCloudflareFunctionsormake up.full.
Other directories: bespoke/ (self-contained custom data-viz components embedded in articles via Shadow DOM; each project under bespoke/projects/ has its own build — see bespoke/readme.md), devTools/ (various utilities).
Key facts that span multiple directories:
- Grapher is a client-side visualization library: a chart is a JSON config stored in MySQL alongside the data values it renders. Chart components follow a three-layer pattern — layout-independent
*State.tsclass, MobX@observer*Chart.tsxcomponent, stateless SVG render component — with aSeries → SizedSeries → PlacedSeries → RenderSeriesdata chain. Readdocs/agent-guidelines/chart-components.mdbefore touching chart code. - The public site is statically baked:
baker/merges Google-Docs-authored content with chart configs from the admin and writes out a static site. Production bakes go through a deploy queue (baker/startDeployQueueServer.ts);make local-bakedoes a full local bake. - Content is authored in Google Docs using ArchieML. The ingestion pipeline lives in
db/model/Gdoc/(gdocToArchie→archieToEnriched→ enriched JSON blocks persisted toposts_gdocs), with aGdocBaseclass hierarchy (GdocPost,GdocDataInsight,GdocHomepage, …). Before working on gdocs-related things, read:docs/agent-guidelines/gdocs-cms-pipeline.md— the archieml pipeline from gdocs to the databasedocs/agent-guidelines/gdocs-class-hierarchy.md— the gdoc types and how to create new onesdocs/agent-guidelines/gdocs-attachments.md— how attachments give rendering components their context
- Table documentation lives in
db/docs/— aREADME.mdoverview plus oneTABLE-NAME.ymlper table. ALWAYS listdb/docs/and read the relevant table files before constructing a query or writing a migration. yarn query 'SELECT ...'— read-only SQL against the local dev DB.yarn query -s "..."queries the staging database for the current git branch (e.g. on branchimages-pageviewsit connects tostaging-site-images-pageviews).- DB access convention in code: wrap queries in
knexReadonlyTransaction/knexReadWriteTransactionfromdb/db.tsrather than using a raw knex instance.
Use the test-on-staging skill when checking a change on staging-site-<branch> — reading or patching a chart's config without a browser, the preview link to share, and Algolia/search caveats.
When AI-authored text is posted through a human's account, it must be clearly attributed so readers do not mistake it for the human's own words.
-
Attribute posts made under a human identity. Any prose that will appear under a human user's GitHub or Slack account — PR bodies, issue and review comments (including replies to Codex/Copilot/reviewers), Slack messages, or drafts intended for a human to post — must start with this blockquote:
> _Written by <model provider> <model name> — @<handle> at the wheel._Use the actual provider and model (e.g. "Claude Sonnet 5", never "Code" or a bare version number) and the handle of the human directing the work, usually the authenticated or current git user. Ask if it is ambiguous.
Do not add this attribution when posting through an account that is clearly identified as an AI agent or bot, such as the Codex GitHub user. In that case, the posting identity already provides the necessary disclosure, and claiming that a human is "at the wheel" may be inaccurate.
A bare mechanical token with no prose, such as a lone
@codex reviewping or 👍, also requires no attribution.If the posting identity cannot be determined, assume it is a human account and include the attribution.
-
Never guess GitHub handles — a wrong
@-tag pings a real person. Use the exact handle from the list below; if a name isn't on it, write the plain name (e.g. "Bastian") and ask the user for the handle.Max Roser @maxroser Esteban Ortiz-Ospina @eoo-owid Edouard Mathieu @edomt Joe Hasell @JoeHasell Hannah Ritchie @HannahRitchie Daniel Bachler @danyx23 Fiona Spooner @spoonerf Tuna Acisu @antea04 Pablo Arriagada @paarriagadap Bastian Herre @bastianherre Bertha Rohenkohl @bertharc Charlie Giattino @CGiattino Pablo Rosado @pabloarosado Lucas Rodés-Guirao @lucasrodes Matthieu Bergel @mlbrgl Marcel Gerber @marcelgerber Sophia Mersmann @sophiamersmann Martin Račák @rakyi Ike Saunders @ikesau Mojmír Vinkler @Marigold Bobbie Macdonald @bnjmacdonald Marwa Boukarim @mrwbkrm Natalie Reynolds-Garcia @natreygar Angela Wenham @angelawenham Valerie Rogers Muigai @ValRMuigai
-
This repo is public — keep internal context out of it. PR descriptions, commit messages, and comments must not identify people who contact us (say "a reader pointed out …" instead of names/roles/employers) and must not reference internal discussions (Slack, Notion) or who suggested what internally. Motivate changes using public facts only.
- Formatting is oxfmt, not prettier — prettier isn't installed. Run
yarn fixFormatChangedinstead of reaching for a formatter yourself. - Double quotes for string literals.
- Type function params and return values; reuse existing shared type definitions. Avoid
any— only use it if you have to, and ask for permission. packages/@ourworldindata/*/**(and a few other dirs) additionally enforce return types viaexplicit-function-return-type/explicit-module-boundary-typesoverrides in.oxlintrc.jsonc;site/**doesn't. Moving a function fromsite/into a shared package therefore needs an explicit return type added, oroxlint --deny-warningsfails.- In Grapher and the admin (MobX 6) we use a nonstandard setup: class-based components with TC-39 stage 3 decorators, but only for
@computedand@action. Observable props are NOT marked@observable; they are listed in amakeObservablecall in the constructor. That call must mention all observable props, but none of the@computed/@actionones. - CSS: named style classes following BEM in separate
.scssfiles; avoid inline styles unless the component already uses them for a similar case. Components usually have a companion scss file with the same name. Entry points:site/owid.scssfor the site,packages/@ourworldindata/grapher/src/core/grapher.scssfor grapher. - In SCSS, do NOT use the parent selector to concatenate BEM class names (
&__element,&--modifier) — write out full class names (.block__element) so it's easy to grep between JSX and SCSS.&with pseudo-classes/elements or state attributes (&:hover,&::before,&[data-selected]) is fine. - Check docs/browser-support.md before using modern JS or CSS features. It lists our supported browsers, the "most breaking" features we rely on, and features we can't yet use.
- For inline
<script>JSON, useescapeJSONStringForInlineScriptfrom@ourworldindata/utils— it also escapes U+2028/U+2029. - package.json scripts are camelCase and descriptive:
startXXXfor long-lived processes,buildXXXfor scripts that write output (docs/coding-style.md). Server-side scripts run viatsx --tsconfig tsconfig.tsx.json.
docs/agent-guidelines/holds in-depth docs written specifically for agents — check there first when working in those areas.- When creating new skills in
.claude/skills/, includemetadata: { internal: true }in the SKILL.md frontmatter unless explicitly asked for a public skill — this keeps external skill indexes from listing internal skills. - GitHub Actions references must stay pinned to commit SHAs (managed with pinact).
If you are running in a Claude Code cloud sandbox (CLAUDE_CODE_REMOTE=true), read docs/agent-guidelines/cloud-sandbox.md BEFORE starting work — it covers renaming the pre-created claude/ branch, database freshness, thumbnails, screenshots, and how to hand the user a staging link.