Back

Turning the Claude Code or Codex you already pay for into a course tutor, inside an ordinary desktop app.

Lerience is a desktop app for taking a real course from a frontier model. It points the Claude Code or Codex the learner already has at a folder on disk, where a tutor protocol makes the model interview them, plan a course, teach, and withhold answers. I built the protocol, the scripts that keep the model honest, and the app around them, and shipped it for Windows and macOS in summer 2026.

Lerience course workspace: the course rail with module progress on the left, a typeset lesson in the middle, and the tutor seminar on the right
The course workspace mid-course: rail, lesson, and seminar, on the demo course the landing page also runs.
Role
Full-stack developer, independent project
Timeframe
Summer 2026
Product
Open-source desktop app for Windows x64, Apple Silicon Macs, and Intel Macs
Core stack
Electron, TypeScript, React, Claude Agent SDK, Codex App Server

Overview

Learning from a frontier model works until you close the tab. Next week it has no idea what tripped you up, nothing you did was checked, and at no point did it have a reason to refuse you the answer. Developers already know the fix, because it is how they work with agents: state in files and Git, rules in a markdown file, checks to run against your work. It also assumes you live in a terminal.

Lerience takes the folder and the markdown file and puts an ordinary app around them. No account, no server, no terminal. The tutor is the learner's own installed Claude Code or Codex, running at full capability in a real course folder. The app is a lens over the course files, a conductor for the agent and the engine scripts, and a stage for the interactive visuals the tutor writes.

It is independent and open source, built over about two months in summer 2026, through three versions before this one: a course I built for myself, a protocol for developers, and a short-lived hosted product. The source, the 30 decision records, and the release evidence are all public.

Four versions, two of them wrong

The idea started as a course, not a product. I wanted to learn AI engineering fundamentals properly, retrieval, agents, evaluation harnesses, and I wanted a frontier model to teach them the way a good mentor would: interview me, plan an arc, make me build things, refuse to do it for me, and remember what tripped me up last week. So I wrote that down as rules in a CLAUDE.md, ran Claude Code in the folder, and took the course. The rules kept getting sharper because the model kept finding ways to be helpful in the wrong direction.

Once the rules were about teaching rather than about AI fundamentals, the course environment became praxeum: a reusable tutor protocol, a course format, four deterministic scripts, and a local web UI, in a Git clone you ran with npx. It proved the pedagogy, and it proved that prose discipline fails: every failure the course audit found was an enforcement failure, not a design failure, which is what produced the doctor, quiz, validate, and QA scripts. What praxeum could not do was reach anyone who would not install Node and open a terminal.

So I built the obvious product, a hosted version: Next.js, Dockerized session runners, Supabase accounts, a Git repo per course. It reached a complete production lifecycle for exactly one user, me, and died in three findings. A managed login on the server invalidated the same account's tokens elsewhere, which no amount of container isolation can fix. A strict-isolation local relay was impossible on the tooling of the day. And a tutor limited to typed tools cannot write scaffolds, run sealed reference solutions, or perform the QA ritual. Same model, amputated agent, plus a token cost nobody would pay.

The desktop app is what those failures point at. Subscription authentication only exists inside the vendor's own client on the learner's machine, so the tutor runs there, at full capability, and every topology that bridged a hosted surface to a local agent disappears once the UI and the agent share one process tree. I wrote the spec and the first thirteen decision records before the first line of app code.

The protocol is the product

A course is a folder. CLAUDE.md in that folder is the tutor protocol, about 200 lines of plain markdown that Claude Code reads natively and AGENTS.md points Codex at. It opens with the prime directive, never write solution code. The learning happens in the gap between the scaffold and the passing checks, so the tutor may explain, ask, point at the line, and reveal hints one level at a time, and may not fill the gap even when asked directly.

Every other rule was earned from a real failure, and the file says so next to each one. Onboarding is a conversation, not a form; it has to settle a capability rather than a topic, the learner's honestly probed background, real hours per week, and a deadline before a course plan is drafted. The learner then reviews that plan before a single module exists, as a hard gate, because building module 00 on an unreviewed plan was the failure mode. Only the current module ever exists in full: the tutor writes the next one when the learner arrives, shaped by how the last one went.

Hints escalate from a nudge, to the approach with nothing pasteable, to near-spoiler pseudocode; if hint two contains code the learner could copy, it is a hint three and gets demoted. Before the learner sees a module, the tutor writes a sealed reference solution, proves the checks pass against it and fail against the stripped scaffold, then deletes the reference. The journal has to be specific: confused X with Y, not did the topic. The tone rule is a single example: that passes, but why is this approach a problem at scale.

What I built
  • Module contents: a LESSON.md written like a textbook chapter, a runnable scaffold with the load-bearing parts cut out, checks the learner runs, three sealed hints, retrieval questions, and an optional visual.
  • Session open runs doctor first, then a recall quiz of the most overdue items, never silently skipping the backlog.
  • Session close is atomic: progress, quiz bank, journal, and commit land together, then doctor verifies.

The model proposes, code decides

The tutor keeps every capability it has: shell, filesystem, web, MCP servers, its own sandbox and approval settings. What it does not get is authority over the record. Every state transition that has to be correct is computed or verified by a script, and the app never interprets model prose as state.

quiz.mjs owns spaced repetition: the tutor judges an answer, the script does the interval arithmetic, because a hand-edited reseed once flattened every earned interval and a module's quiz silently never came due again. doctor.mjs refuses to open a new session on top of an unclosed one. validate.mjs checks every module against JSON schemas. qa-module.mjs runs the sealed-reference ritual and lints the materials, down to flagging a test that compares two measured durations against each other, because that exact flake once reached a learner.

The app takes the same stance. At session open it runs doctor and the due-list script itself and prepends the facts to the opener message, so the did-the-tutor-actually-read-the-files failure mode closes deterministically. The course rail, due counts, and record view are parsed from the files by the app. Capability and authority are different layers; conflating them was the hosted version's mistake.

Driving the learner's own agent

The agent seam is small on purpose: a TutorAgent that starts a session in a course folder, and an AgentSession with one event stream, send, respondToApproval, interrupt, and end. The renderer consumes one pushed IPC stream and never sees a provider shape.

Claude Code runs through the Agent SDK with the discovered executable and the learner's own settings, so the session is equivalent to running claude in that folder. Codex runs through its App Server over stdio, which has no CLAUDE.md convention, so the adapter reads the course's protocol file and hands it over as developer instructions. Both adapters carry the invariants learned from the hosted failure: never touch provider auth, never replace HOME, and keep credential-shaped content out of transcripts.

Session controls came from watching real runs. The learner could not tell which model was answering, and a course build raised about thirty approval prompts. So the app offers model, effort, and an autonomy ladder, applied through the provider's own runtime settings, session-scoped, never written to the learner's own config. Current values are what the provider reports back, not what was requested. The Never ask rung was added after the ten-minute approval queue taught people to click Allow without reading; an honest opt-out beats habituated consent.

Lerience tutor gate with two provider cards, Claude Code not signed in and Codex connected under a fixture account
The tutor gate on synthetic fixtures: one provider is not signed in yet, the other is connected and names the account that would teach.
What I built
  • Provider discovery through well-known install paths and the inherited PATH; a per-provider version contract turns drift into a readiness state with official install guidance.
  • Sign-in is the vendor's own browser ceremony, on explicit learner action. No API keys, no pasted tokens, no logout button, because those credentials are shared with the learner's other clients.

Sessions that survive walking away

The most motivated real user of the prototype never closed a session cleanly. Moving on mid-session and coming back the next day was the actual usage pattern, and the protocol's close ritual is where the cross-session memory gets written. So closes matter, and clean closes must not be load-bearing.

Lerience assumes abandonment is the normal end of a session. The transcript checkpoints continuously to app-data, and course files are whatever the tutor already wrote. No timer fires, no unattended agent runs. At the next open, the app detects the unclosed session and has the tutor wrap it up from evidence, then doctor verifies and the new session opens with the recall quiz. An explicit End session button runs the same ritual eagerly.

Git keeps the role it had in the prototype and nothing more: the tutor commits at close, doctor verifies, history is the audit trail. Copying the course folder to another machine moves the whole course. The transcript store is append-only JSONL, fsynced per entry, and it redacts credential-shaped text as it streams, including secrets split across two deltas, without tripping on a course that legitimately teaches HTTP auth.

Lerience onboarding: the tutor asks three interview questions before drafting a course
01 / 03
Onboarding is the tutor's interview, rendered as a conversation; the app owns no wizard state of its own.

The app is a stage

When a module turns on something spatial or dynamic, the tutor writes a self-contained HTML visual, derived from the lesson it just wrote so the examples and vocabulary match, and embeds it in the chapter where the picture belongs. If a misconception surfaces mid-session, it can retarget the visual at it on the spot. Visuals are served over a private scheme with a default-src none CSP and mounted in an iframe with no same-origin grant: a null origin, no network, no IPC, no filesystem. Self-contained is a property the CSP enforces, not a rule the model is asked to follow.

The decision record behind this refuses a fixed widget kit on purpose. Building things is where frontier models are improving fastest, and a course that writes its own vector-math manipulative for the learner struggling with projections teaches something prose cannot. The app defines the frame and the safety contract, never the vocabulary.

Lerience lab overlay showing a vectors and similarity visual with two draggable arrows and live cosine similarity and dot product readouts
The Lab overlay on a vectors module: drag either arrow and the cosine, dot product, and angle update together.

An installed app, not a terminal

The interface is designed to feel like an installed application whose material is the learner's course, not a chat wrapper. Chrome is neutral monochrome; colour is reserved for attention, repair, success, and failure. Type follows ownership rather than heading versus body: Inter is the app's voice, Literata is the course's voice for lesson prose and tutor turns, JetBrains Mono is for code and readouts. The raw values live in one tokens file, Tailwind is derived from it with its stock palette cleared, so a non-token colour utility does not compile, and a test computes WCAG contrast for every ink on every ground in both themes.

The workspace is a rail, a material pane, and the seminar, in one app-drawn window frame that keeps the native caption buttons on both platforms. Affordances follow file presence: Run checks and Open in editor render only when the current module actually has a scaffold or checks, so a history course never shows them and no course-type classifier exists. The learner writes code in their own editor, which the app detects and names. A browser harness mounts the production components against fictional fixtures, and the landing page embeds that same build with a scripted tutor, so visitors see the real interface running in the page.

Fail closed, all the way to the installer

There are three trust boundaries. The renderer runs sandboxed and sees only a narrow typed preload API, every argument re-validated in the main process. The main process owns course access, sessions, provider discovery, updates, and the runtime. Provider clients stay separately installed and authenticated; Lerience discovers them through explicit native paths and never bundles or updates them. The first runtime I packaged measured close to a gigabyte, most of it the two provider clients, which is what forced that rule. What ships is the Course Engine, a portable Git, and npm, with Electron's own Node reused through shims.

Every packaged launch verifies its runtime before the app starts: the manifest must belong to this app version and platform, and every critical file must match its recorded size and SHA-256. A few thousand files, under a second warm, and anything short of a clean pass shows a repair dialog and quits. The same check runs headless as the release-acceptance contract.

Updates are learner-approved and verified before they run. Release metadata is signed with an app-owned Ed25519 key, and each artifact is bound by filename, byte size, and SHA-256. The release workflow runs from an annotated tag on a protected main and can only create a draft; a human publishes after downloading the bytes back and verifying them, and the evidence record lists every hash.

What I built
  • Engine updates for an existing course are a separate, explicit operation: read-only preview, refusal on any learner edit, the commit built in a temporary clone and fast-forwarded only if the repository is unchanged. An app update never rewrites a course.
  • Windows and macOS packages are unsigned at the OS level for now. Windows may show an unknown-publisher warning, while macOS requires first-launch approval in Privacy & Security.

Outcome

Lerience ships public packages for Windows x64 and native macOS builds for Apple Silicon and Intel. It is early, unsigned at the OS level, and experimental on provider policy. The repo carries a spec, 30 decision records with explicit reopens-if clauses, about 20k lines of application code, 48 test files, and release evidence with every hash. Live incidents became regression tests: the turn that reported cost and completion while streaming nothing, the model picker that reset itself, the contrast token that was fine until a human squinted at a laptop in a bright room.

What I value in this one is the discipline around a model you cannot fully trust. The tutor keeps every capability it has, and the app refuses to take its word only where a wrong answer would corrupt the record. That split between capability and authority runs from the quiz script to the release manifest. It is also the project where I most worked the way the essay on this site describes: directing agents from a folder of written rules, and spending my own attention on the decisions and the seams.