Dojo is a lesson app built on the Condensate runtime, using the same engine and SDK as the rest of Condensate. Each lesson gives the learner a task, a tutor that reviews the work, and a place to do it: a terminal for programming lessons, or a notebook for work that is written out and checked step by step. This page covers running Dojo, writing lessons, the tutor, sign-in and deployment.
Start Dojo
Run it from the repository root:
bun run --cwd apps/dojo dev
Open http://127.0.0.1:4800. dev restarts the server when the source changes. start runs the same server without watching files.
| Setting | Effect |
|---|---|
DOJO_PORT | Port to listen on. Defaults to 4800. 0 picks any free port. |
The server listens on every network interface, not only on localhost. Before you expose it, turn on sign-in (see Sign-in and access).
Where Dojo keeps its files
Dojo stores everything under one folder, DOJO_HOME. It defaults to ~/.dojo.
| Path | Holds |
|---|---|
config.json | Setup state, including the student's Claude token if one was saved |
generated/<id>/ | Lessons written by the course author |
curriculum/ | Learning paths created from a topic |
workspaces/<id>/ | The learner's working folder for each lesson |
progress.json | Completed lessons, completion days, attendance days, and mastery tiers |
sessions/ | The visible chat history for each lesson |
engine-state/ | The tutor engine's resume pointers |
beta.json | Beta codes and the addresses that redeemed them |
author-failures/ | Lessons the author could not write, kept for debugging |
student/drills.jsonl | Drill events: issued and done |
student/record.jsonl | The student record: notes the tutor keeps across sessions |
Lessons you write by hand live in the lessons/ folder of the app. Generated lessons live in DOJO_HOME. If a generated lesson has the same id as a hand-written one, the hand-written lesson wins.
What a lesson is
Each lesson is a folder with lesson.json (the manifest), lesson.md (the brief the learner and tutor read), and an optional setup.sh that seeds the working folder the first time the lesson opens. The manifest sets the things that shape the session:
| Field | What it does |
|---|---|
mode | shell gives a terminal. workbench gives a notebook. |
track | The course the lesson belongs to. Lessons unlock in order within a track. |
tutor | Settings for the tutor for this lesson. |
goals | The skills the lesson checks, in order. They appear as the learner's checklist. |
shell | The command for the lesson terminal. Defaults to a login zsh. |
homeIsWorkspace | Sets HOME to the working folder, so edits to dotfiles such as .zshrc stay inside the lesson and never touch the real home folder. |
Set DOJO_UNLOCK_ALL=1 to open every lesson regardless of progress. This is for development and testing.
The learning flow
The learner opens a lesson or asks for a generated learning path. The tutor reads the current state, gives one focused task, reviews the attempt, and picks the next step. When the learner comes back, the tutor reads the notebook and earlier attempts first. It continues an unfinished question instead of adding a new one or repeating the introduction.
The screen
A lesson shows the tutor on one side and the work on the other. The work is either a terminal (shell mode) or a notebook (workbench mode). Two controls shape the help the learner gets:
- Check my work asks the tutor to review what the learner has done so far.
- I'm stuck asks for help. The help comes in steps. The first step gives no answer. After one attempt the tutor may give a hint, and after two attempts it may give a worked example. Dojo counts the attempts, so the help stays one step behind the learner's own effort.
The tutor also watches the work. It has two modes for this:
| Mode | What happens |
|---|---|
guided (default) | The tutor comments on the work as it changes, after the learner pauses. It waits a short time between comments. |
on-request | The tutor stays quiet until the learner asks for a check. |
The learner picks the mode with the observe buttons on the screen.
Where commands run
Programming lessons run commands. Dojo decides where they run:
- Docker, per lesson. Each lesson gets its own container, named
dojo-<lesson id>, built from thedojo-sandboximage. The learner's folder is mounted at/home/student, and the lesson's files are mounted read-only at/opt/lesson. Each container is limited to 1 GB of memory and 512 processes. - Host, as a fallback. If Docker is not running, Dojo falls back to the host and logs a warning.
- Off. Set
DOJO_SANDBOX=offto skip Docker on purpose.
The learner's terminal runs in the container when Docker is available. The tutor's own Bash tool is different. In bun run dev on the host, the tutor runs its commands on the host, in the lesson's working folder. When a lesson container is in use, the tutor is told to run commands with docker exec <container> so that they run inside the sandbox.
The Dojo container image is a different case. It sets DOJO_SANDBOX=off and relies on the container itself as the boundary. Do not run Dojo on a machine you care about without the container if the lessons run untrusted code.
Terminal sessions live on the server, not in the browser. Each one keeps the last 256 KB of output, so a reconnect shows where the learner left off. A session that has been idle for 2 hours is closed. Change the limit with DOJO_PTY_IDLE_MS (in milliseconds).
The tutor
The tutor is one engine session per lesson. It runs on the Claude engine, with the model set by DOJO_TUTOR_MODEL (default claude-sonnet-5-5). The tutor is allowed to:
| Tool | Use |
|---|---|
Read, Glob, Grep | Read the lesson and the learner's files |
Write, Edit | Change files in the working folder |
Bash | Run commands, as described in Where commands run |
Dojo tools (read_terminal, publish_surface, mark_goal, and others) | Read the terminal, show a surface, record progress |
The tutor cannot ask for permission in the middle of a turn. Its permission mode is dontAsk, so anything outside the list above is refused. A single tutor run stops after 100 turns.
Verification. The tutor should confirm a goal from the learner's files, the terminal output, or the answers in a quiz before it marks the goal done. It does not mark a goal done because the learner says so.
Memory across restarts. Dojo keeps the visible chat history for each lesson in sessions/, up to the last 500 events. The engine keeps a pointer to its own session in engine-state/. When the server restarts, the tutor resumes that session, so the conversation continues where it stopped. If the visible history is empty, Dojo starts a fresh session.
The notebook
Workbench lessons use a notebook. A notebook is a list of cells. Each cell has a kind and an owner:
| Kind | Use |
|---|---|
explanation | Text that teaches the idea |
working | The learner's written steps |
question | A question for the learner to answer |
calculation | A small function the learner runs to check a value |
Owners are learner or tutor. Every edit creates a revision, and a write must say which revision it is based on, so two edits cannot silently overwrite each other. Each notebook is stored as notebook.json, or as notebook-<student>.json when more than one student uses the same lesson. The file is limited to 4 MB.
Calculations. A calculation cell runs JavaScript (TypeScript source with the types removed) in QuickJS, a small JavaScript engine, inside a separate Bun process. The limits are:
| Limit | Value |
|---|---|
| Run time | 250 ms of script time. The whole process is killed after 3 seconds. |
| Memory | 16 MB |
| Runs at once | 4 |
| Imports | Not allowed |
Calculations are not Python. A learner can submit an answer to a question cell, which saves the answer against the current revision. The learner can also ask for coaching on the notebook as a whole, and that request goes to the tutor.
Progress, mastery, and drills
Mastery. When a goal is passed, Dojo records a tier for it without showing it to the learner. clean means the learner passed without help. assisted means help was needed. A later clean pass replaces an earlier assisted tier. When the author writes a later generated lesson, it can add a cold re-test of one goal still marked assisted. The learner gets that skill again without a hint, and a clean pass upgrades the tier.
Drills. A drill is a short retrieval question about an earlier goal. Drills come back on a schedule of 1, 2, and 5 days after each rep. A drill graduates when it has been done four times, and then it stops coming back. Each rep is logged in student/drills.jsonl.
Attendance. Dojo records the days the learner shows up, using the learner's local date. At the end of a session the learner sees how many of the last 7 days they came. Dojo does not track streaks, and missing a day does not reset anything.
Reflection. After a session, a small reflector model (DOJO_REFLECTOR_MODEL, default claude-haiku-5-5) reads the transcript and writes 0 to 3 observations about the learner into the student record, student/record.jsonl. It also proposes up to 2 drills. It does not repeat observations that are already in the record, and it writes nothing when nothing new and durable came up. The tutor can read and add to the record through the student_record_read and student_record_note tools.
Check-out. When a lesson is passed, the tutor asks the learner to rate their own understanding. The rating is saved to the student record with a self: prefix, so it can be told apart from the tutor's notes.
Generated courses and exercises
Dojo can build lessons from a topic. Three parts do this work:
| Part | What it does | Model setting |
|---|---|---|
Forge (POST /api/paths) | Turns a topic or source text into a learning path in curriculum/. Source text is capped at 20,000 characters. | DOJO_FORGE_MODEL, default claude-sonnet-5-5 |
| Author | Writes the lesson files for each entry in a path into generated/<id>/ | DOJO_AUTHOR_MODEL, default claude-sonnet-5-5 |
| Tutor surfaces | Exercises the tutor shows in the page | The tutor model (DOJO_TUTOR_MODEL) |
Forge and Author each run as a single one-turn engine session, not as a long-running agent. Surfaces come from the lesson's own tutor session.
Surfaces. The tutor can publish an interactive exercise with publish_surface. A surface is a React component written in TSX. It may import only react, react-dom, and the @dojo/ui kit. It may not call fetch, XMLHttpRequest, WebSocket, or localStorage. The source is limited to 64 KiB, and it must build before it is shown. The surface sends events back to the server, and the tutor reads its state with read_surface_state.
Quick visuals. For a small chart or diagram, the tutor can use a quick visual. A quick visual is checked against a schema and never runs code. Its only output is a fixed set of action buttons, which Dojo turns into messages in the tutor chat.
Sign-in and access
Dojo has three ways to control who gets in. They can be used together.
| Method | How it works | Set with |
|---|---|---|
| Password | A password cookie (dojo_auth), signed with HMAC | DOJO_PASSWORD |
| Control-plane token | A signed token passed as ?dojo_token= once, then kept in a cookie (dojo_jwt) | DOJO_JWT_SECRET |
| WorkOS sign-in | Sign-in through WorkOS AuthKit at /api/oauth/login, with a callback at /api/oauth/callback | WORKOS_CLIENT_ID, WORKOS_API_KEY, DOJO_JWT_SECRET |
If neither DOJO_PASSWORD nor DOJO_JWT_SECRET is set, sign-in is off. Set one of them before you expose Dojo on a network.
For WorkOS, DOJO_ALLOWED_EMAILS is a comma-separated list of addresses that may sign in. If it is empty, anyone who can sign in with WorkOS gets in. A user can also be approved by redeeming a beta code. Any signed-in user can create a code through POST /api/beta/new, with a maximum number of uses from 1 to 50 (default 1). Approved addresses are kept in beta.json under DOJO_HOME.
Tutor login
The tutor needs a Claude login. Dojo tries these sources in order and uses the first one that works:
- Student token. When no Condensate runtime answers, the setup screen offers
claude setup-token. The learner runs it, and the token is saved toconfig.json. Dojo passes it to the tutor asCLAUDE_CODE_OAUTH_TOKEN. - Runtime account. Dojo asks the Condensate runtime for a Claude account through its token route. The runtime is at
CONDENSATE_AGENTS_URL, which defaults tohttp://127.0.0.1:4741. SetDOJO_ACCOUNTto name the account. If you do not, Dojo picks the first Claude login that is not over its session limit, or the first one listed if all of them are. - Host login. The Claude login already on the machine.
Credential proxy. The credential proxy keeps real credentials out of the agents. It is on by default, as described in accounts. The token route answers only callers on the same machine as the runtime. A container that reaches the host through host.docker.internal counts. When the proxy is on, the route also asks for a broker-peer bearer token, which Dojo does not send yet. So the runtime account (option 2) is refused until that is wired up. A student token from option 1 still works, because it does not go through the token route. For how accounts work in general, see accounts.
Configuration
| Variable | Default | What it sets |
|---|---|---|
DOJO_PORT | 4800 | Listening port. 0 picks a free port. |
DOJO_HOME | ~/.dojo | Folder for all Dojo state |
DOJO_SANDBOX | Docker when available | Set to off to skip Docker |
DOJO_PTY_IDLE_MS | 2 hours | How long an idle terminal session is kept |
DOJO_UNLOCK_ALL | off | Set to 1 to unlock every lesson |
DOJO_TUTOR_MODEL | claude-sonnet-5-5 | Tutor model |
DOJO_REFLECTOR_MODEL | claude-haiku-5-5 | Model that writes session observations |
DOJO_AUTHOR_MODEL | claude-sonnet-5-5 | Model that writes lesson files |
DOJO_FORGE_MODEL | claude-sonnet-5-5 | Model that builds learning paths |
DOJO_ACCOUNT | First available | Named Claude account from the runtime |
CONDENSATE_AGENTS_URL | http://127.0.0.1:4741 | Condensate runtime URL |
DOJO_PASSWORD | unset | Turns on password sign-in |
DOJO_JWT_SECRET | unset | Turns on control-plane token sign-in and is needed for WorkOS |
WORKOS_CLIENT_ID, WORKOS_API_KEY | unset | WorkOS sign-in |
DOJO_ALLOWED_EMAILS | empty (anyone who can sign in) | Comma-separated addresses allowed in with WorkOS |
Deploy
Docker Compose. The docker-compose.yml file in apps/dojo runs one service, dojo. It publishes port 4800 on 127.0.0.1 only, so the server is reachable from the machine but not from the network. It stores its state in the dojo-data volume, mounted at /data, with DOJO_HOME=/data/.dojo. The image is built from the monorepo root, and it installs the tools lessons need: zsh, git, nano, vim, ripgrep, jq, python3, and a C toolchain. The image also includes Bun and the Claude Code CLI.
In compose, CONDENSATE_AGENTS_URL defaults to http://host.docker.internal:4741, so the container asks the runtime on the host for the runtime account.
Move to a Fly Sprite. When the sprite CLI is installed and logged in, Dojo shows a button that moves the app to a Fly Sprite. The move copies all of DOJO_HOME, including the saved Claude token, to the Sprite. It also passes on the sign-in and model settings. The Sprite gets a public URL and runs with DOJO_SANDBOX=off. A password always protects it: your DOJO_PASSWORD, or a generated one shown when the move finishes. The local server keeps running. If the move fails, the Sprite is destroyed and the local copy is left as it was. Treat the Sprite as holding the same credentials as the local copy.
Checks
From apps/dojo:
bun run typecheck
bun run test
test runs the Dojo test suite through a script that checks for leftover temporary files.