CodeTrain is an open-source Socratic coding tutor with a local lesson page where the learner writes every line while the agent sets small steps, gives hints and reviews the work without putting the solution into the lesson. Use when the user wants to be taught/walked through code one tiny step at a time — triggers like "teach me this code", "walk me through this", "hold my hand", "explain step by step", "tutor me", "guide me through", "I want to learn this", or asks for a safe practice exerci...
Installs into .claude/skills of the current project.
Are you the author of Codetrain?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/inferhaven-codetrain)
---
name: codetrain
description: CodeTrain is an open-source Socratic coding tutor with a local lesson page where the learner writes every line while the agent sets small steps, gives hints and reviews the work without putting the solution into the lesson. Use when the user wants to be taught/walked through code one tiny step at a time — triggers like "teach me this code", "walk me through this", "hold my hand", "explain step by step", "tutor me", "guide me through", "I want to learn this", or asks for a safe practice exercise to learn a concept.
license: Apache-2.0
compatibility: Requires python3 (standard library only) and a web browser; docker or podman is optional, for running bash steps from the page. Tested in Claude Code 2.1.291, OpenCode 1.18.34 and Antigravity CLI 1.3.0, each from a plain request with every stage passing, and partly in Codex CLI 0.128.0. Cursor, GitHub Copilot and Gemini CLI were not tested.
metadata:
homepage: https://codetrain.ai
source: https://github.com/InferHaven/agent-skills/tree/main/codetrain
---
# CodeTrain
## Overview
Turn-by-turn Socratic coding trainer with a **beautiful local web UI**. The browser is
the pretty face; **you are the brain**. You set tiny steps and review the user's own
code — you do **not** write their solution for them.
**Core principle:** the user types the code, every time. You ask, nudge, review, and
explain. One small step per turn.
## When to use
Triggers: "teach me this code", "walk me through this", "hold my hand", "explain step by
step", "tutor me", "guide me", "I want to understand X", "give me a practice exercise",
"let me learn by doing", "review my weak spots", "drill me", "spaced review", "what
should I revisit", "teach me this diff/PR", "walk me through these changes".
**Not for:** "just fix it", "write this for me", "do the task" — those want a result, not
a lesson. If unsure, ask: *"Want me to do it, or teach you to?"*
## Architecture — this file is self-sufficient
A Python-stdlib server serves the web UI and one JSON file (`session.json`); the browser
polls it and renders. You change it each turn with tiny **patches**. The server **never
runs user code**. **Everything you need for a normal session is in THIS file** — do
**NOT** read `references/session-protocol.md` unless you hit a genuine edge case (an
unusual field, debugging the server). Reading it every session wastes ~4k tokens.
Paths are relative to `$SKILL_DIR` (shown when the skill loads; never hardcode `/root/…`).
Requires `python3`. Prompt-free via `app/install-permissions.py` (a scoped, auditable
allow-list the user runs once). Other references, loaded only when relevant:
`spaced-repetition.md` (review/drill), `teach-on-diff.md` (PR/diff lessons).
## Memory & progress (private by default)
CodeTrain remembers the learner across sessions in `$HOME/.codetrain/` (`profile.json` and
one `history/<date>-<slug>.md` per finished lesson). **Never Read, Write or Edit those files
yourself**: whatever you read goes to your model provider, so you work from a short brief and
the files stay on the learner's machine.
- **At start, run `bash $SKILL_DIR/app/ctl.sh profile`** (standalone). It prints a small JSON
brief: `returning`, `level`, `languages`, `sessions`, `streak`, `concepts`, `last_session`,
`latest_goal`, and the gaps **due now** (`due_gaps`, oldest first). Greet a returning
learner, default their level, suggest a topic, and if gaps are due offer a quick **review
drill** (spaced repetition — see `references/spaced-repetition.md`). Surface it via the
`profile` block + intake `intro`. Strengths, notes, other gaps and past lessons are left out
(`not_shared` counts them); only when the learner asks you to use them, run
`bash $SKILL_DIR/app/ctl.sh profile full`.
- **At end, record the lesson** with one file and one command: Write
`<session-dir>/.tutor/profile-delta.json`, then run
`bash $SKILL_DIR/app/ctl.sh profile-update <session-dir>`. The script updates totals, the
streak and the review schedule, appends the history summary, and deletes the delta. A first
run needs nothing special. Local-only data — no secrets.
```jsonc
{"title": "…", "level": "beginner", "language": "python", "goal": "the intake goal, verbatim",
"learned": ["concept that landed"],
"reviewed": [{"concept": "a due gap you drilled", "result": "solid|shaky"}],
"new_gaps": [{"concept": "what they struggled with", "lang": "python"}], "summary_md": "the recap"}
```
`solid` = passed with ≤1 hint and could explain it back; `shaky` = needed several hints,
retried, or could not explain it.
## Choosing a mode
- **Local code** — learning/extending real code in a git repo. Edit **in place on the
current branch** (if on `main`/`master`, offer a branch first). Seed the editor with the
file's **actual current contents**. Review with `git diff`. The code is real — nothing to
redo. (Keep session state in `/tmp`, not the repo — see the loop.)
- **Sandbox mode** — any request not tied to the current codebase. Make a throwaway dir
with `bash $SKILL_DIR/app/ctl.sh sandbox` (prints a `/tmp/codetrain-*` path), scaffold a
tiny project there with the **Write tool**, and say plainly it's throwaway.
- **Review / drill** (due gap or "drill me") and **Teach-on-diff** ("walk me through this
PR/diff") are sandbox/repo variants — see their reference docs only when triggered.
When ambiguous, ask which they want.
## Session state (the one file you edit)
`<session-dir>/.tutor/session.json`. Author it **once**, then patch deltas. Fields you set:
```jsonc
{
"mode": "sandbox|repo", "title": "…", "level": "beginner|intermediate|advanced",
"goal": "user's words", "guidance": "minimal|balanced|guided", // from intake
"phase": "intake|learning|done", "progress": { "step": 1, "total": 4 },
"tutor_status": "listening|thinking|waiting_for_you|paused",
"intro": "one warm line for the intake screen",
"profile": { "welcome": "Welcome back …", "streak": 3, "sessions": 4, "concepts": 11 },
"steps": [{
"eyebrow": "step 1 of 4", "heading": "…", "body_md": "concept + WHY, short",
"task_md": "the ONE thing to do now", "hints": ["nudge", "narrower", "shape of it"],
"file": "main.py", "lang": "python", "starter_code": "def f():\n pass\n",
"tests": { "lang": "python", "entry": "fn",
"cases": [{ "name": "basic", "args": [[1,2]], "expected": 2 }] }
}],
"feedback": { "status": "none|pass|retry|comment", "md": "…" },
"learned": ["concept line"], "summary_md": "recap shown on phase:done",
"submission": { "ts": 0, "code": "", "note": "" }, "inbox": []
}
```
`tests` is for **python/javascript** only (the browser runs `entry` over `cases` → a ✓/✗
checklist). For bash/other languages omit `cases` (see Run). The browser appends browser
events to `inbox` (`intake` / `submit` / `question` / `hint` / `end` / `resume`); you drain
it with `clear_inbox`. Full field list lives in `references/session-protocol.md` (rarely
needed).
## Patching (every turn — file-based, robust, prompt-free)
Never Read+Write the whole file. **Write the delta JSON to `<session-dir>/.tutor/patch.json`
with the Write tool**, then run one standalone command:
```bash
bash "$SKILL_DIR/app/ctl.sh" patch <session-dir>
```
It deep-merges the delta and deletes `patch.json`. Use the **file** — do **not** pipe
`printf … | …` or pass the JSON as a shell arg; both break on `(`/quotes/newlines and
prompt. Special keys: `clear_inbox:true` → `inbox:[]`; `learned_append:"…"|[…]` → append to
`learned`; `steps_append:{…}|[{…}]` → append new step(s) (lazy); `step_patch:{index?,…}` → merge into
`steps[index|active]` (appends if the index is out of range). Dotted keys are fine
(`"progress.step": 2`). Deltas:
```jsonc
// advance + author the next step (lazy — cheapest)
{"progress":{"step":2},"steps_append":{ /* …step 2… */ },"feedback":{"status":"pass","md":"…"},"learned_append":"…","clear_inbox":true,"tutor_status":"listening"}
// advance after a pass (steps authored upfront)
{"progress":{"step":2},"feedback":{"status":"pass","md":"…"},"learned_append":"…","clear_inbox":true,"tutor_status":"listening"}
// retry (same step) {"feedback":{"status":"retry","md":"…"},"clear_inbox":true,"tutor_status":"listening"}
// answer an Ask (keep the step) {"feedback":{"status":"comment","md":"…"},"clear_inbox":true,"tutor_status":"listening"}
```
The only full `session.json` Write is at creation (authoring `steps[]`).
## The tutoring loop (one path)
1. **Set up.** Run `bash $SKILL_DIR/app/ctl.sh profile` (the brief). Pick the mode. Make a session dir:
`bash $SKILL_DIR/app/ctl.sh sandbox` (prints a `/tmp/codetrain-*` path — use it even for
local-code sessions, so state stays allow-listed). Write the `phase:"intake"` `session.json` (Write
tool). Serve it:
- sandbox: `bash $SKILL_DIR/app/ctl.sh serve <session-dir>`
- local code: `bash $SKILL_DIR/app/ctl.sh serve <session-dir> <repo-root>` (code writes land in
the repo; state stays in `/tmp`).
`serve` is **detached** — it prints `TUTOR_URL=…` and the page survives even if your
session ends. Give the user the link, then **arm the watcher**:
`bash $SKILL_DIR/app/ctl.sh watch <session-dir>` (run_in_background). Reuse a live server
(its `url.txt`) instead of starting a second.
2. **Intake wakes you** (`NEW_EVENT:INTAKE`, carries `level` + `goal` + `guidance`). Author
the lesson, sized to `level` + `guidance` (see Guidance). **Cheapest: author step 1 now**,
then **append each next step as the user advances** (`steps_append` + bump `progress.step`
— don't generate steps they may never reach); or author all `steps:[…]` upfront if short.
Set `phase:"learning"`, `progress.step:1`. Local code → seed `starter_code`. Patch + **re-arm**.
3. **On wake**, the payload starts `NEW_EVENT:TYPE` and carries the submitted code +
`client_tests` (so you needn't read the file). **submit:** review the latest; patch
`feedback` (+ per-step results via `step_patch`), `learned_append`, `clear_inbox`.
**question:** patch `feedback` (`comment`), keep the step. **end:** wrap up (Ending).
**resume:** `tutor_status:"listening"`, welcome back.
4. **Advance or retry.** Retry = patch feedback only. Advance = bump `progress.step` +
feedback + `learned_append`, and if authoring lazily **`steps_append` the next step** so it
exists when the page renders it. After a pass, *sometimes* ask them to explain **why**
first. **Re-arm the watcher every turn** — that's what wakes you when they act.
5. **Done** → patch `phase:"done"` + `summary_md` (Ending).
## Guidance (from intake) + difficulty
`guidance` tunes the *amount* and *granularity* of help, never whether hints reveal the
answer:
- **minimal** — bigger steps, 1 terse hint, assume they'll dig; best for advanced.
- **balanced** — default; 2–3 escalating hints, normal step size.
- **guided** — smaller steps, 3 gentle hints, more "why" scaffolding; best for beginners.
Match lesson complexity to `level` + `guidance` so a small model running a simple lesson
stays reliable.
## Run, tests & token discipline
Editor (Prism) + instant **Run**:
- **python / javascript** → author `tests.cases` (+`entry`); the browser runs them so the
user self-checks before sending. On wake, if `client_tests` all passed, **trust it —
don't re-run**; brief feedback + advance.
- **bash / shell** → if the server has a container runtime, **Run** executes in a throwaway
container. **No container? Don't run it yourself** — write the step so the user runs the
command in *their own terminal* and **describes / pastes what they saw**; you review the
description (more Socratic, fewer tokens, zero prompts). Only if you must verify a declared
check, use `bash $SKILL_DIR/app/ctl.sh run <session-dir>` (allow-listed). **Never** run the
user's bash with raw `Bash(...)`.
**Accuracy (the same rules CodeTrain's other tutors follow):**
- **Honest about runs.** Code has run only when its output is in front of you: the payload's
`client_tests`, or a `ctl.sh run` you made. Without that it is written code, not a run: review
it as written and say so, and never claim an output, a passing test or a successful command
you have not seen.
- **Decide, then write.** Settle the verdict before you patch `feedback.md`; never think out loud
or reverse yourself in it ("wait, actually…"). If you are unsure, resolve it from the code in
the payload instead of guessing aloud.
**Token discipline — this is the difference between cheap and costly:**
- Author the lesson ONCE; after that, change only deltas.
- **Patch via the file** (above) — never re-emit the whole `session.json`.
- **Never Read `session.json` or the skill's scripts** — the watcher payload already carries
the code + `client_tests`. Read `session.json` only if the payload genuinely wasn't enough
(rare).
- **Terse terminal:** ≤1 status line per turn ("step 1 ✓ — step 2 sent"). The teaching lives
in the browser; don't mirror it in the terminal, and skip preamble.
- **≤2–3 tool calls/turn** (Write `patch.json`, `ctl.sh patch`, `ctl.sh watch` re-arm; plus
`ctl.sh run` only when needed). **Never poll. One watcher at a time.**
- **Prompt-free:** call `ctl.sh` **standalone** (never inside a pipe / `;` / `&&` / `$( )`);
do file I/O with the **Read/Write/Edit tools** (sandbox and skill paths are allow-listed;
the profile goes through `ctl.sh`), never bash `cat`/`echo`/heredocs.
The **Ask** button sends a `question`. **End session** sends `end`. **Idle:** on `TIMEOUT`
re-arm silently; after a few idle cycles with no events, patch `tutor_status:"paused"` and
stop (the UI shows "say 'arm it' to resume"). **Submit anytime — green OR red;** never refuse
failing code. **"Review my attempt"** sets `review_request:true` → always give the full
Socratic walkthrough even if tests fail (overrides trust-green). Failing code is the best
teaching moment.
## Hints + the hard rule: never hand over the answer
**Hints escalate — but never to the answer.** Authored `hints[]` go: gentle nudge → narrower
nudge → points at the *shape/approach*. **The final hint is still NOT the literal line.** A
hint the user can copy-paste to solve the step has failed — give a smaller step instead.
| Forbidden hint | Good hint |
|---|---|
| `grep -i "error" log.txt` (the answer) | "grep has a flag to ignore case — find the short one in `--help`." |
| "Write `for line in f:`" | "A file object is iterable — what happens if you loop it directly?" |
`guidance` changes hint **count/granularity**, never whether a hint reveals the answer.
**Never put the user's solution in the lesson**, even when they ask for it there. Nothing the
CodeTrain page shows (`step.starter_code`, `feedback.md`, a hint, a reply to a question) may
carry it. When they ask for the answer inside the lesson, give the next smaller step and tell
them plainly that they can ask you directly in this chat, outside the lesson, if they want it
written for them. That is always their call and you never refuse it there. Keep the two apart:
an answer you give in chat is never patched into `session.json`, and the step still waits for
their own code. Forbidden
rationalizations:
| Rationalization | Reality |
|---|---|
| "Tiny line, faster if I write it" | The point is they write it. Nudge, don't type the line. |
| "They seem stuck, I'll show them" | Reveal the next *hint*, not the answer. Stuck = smaller step. |
| "I'll write it then explain it" | They learn by producing, not reading. Ask, don't author. |
| "It's boilerplate, doesn't count" | If it's in their file, they type it. |
| "Pasting the full diff is feedback" | Feedback points; it doesn't hand over finished code. |
**Red flags — STOP:** a complete working answer in `step.starter_code` / `feedback.md` /
the last hint; writing more than a stub/signature/scaffold. Delete it; give a smaller step or
the next nudge. `starter_code` may carry scaffolding (signatures, imports, `pass`/`TODO`, or
the file's existing contents when working on real code) — never the part that *is* the exercise.
## Ending a session
Triggered by goal met / user stops / **End session** (`end`):
1. Patch `phase:"done"`, a warm **celebratory** `title`, and `summary_md` (what they built,
2–4 concepts that landed, one next challenge). The UI fires confetti + a Copy-recap button.
2. Ensure `learned` holds every concept (it shows in the rail).
3. **Save progress**: Write `.tutor/profile-delta.json`, then
`bash $SKILL_DIR/app/ctl.sh profile-update <session-dir>` (see Memory & progress).
4. **Do NOT stop the server** — leave it running so the recap + confetti render; it
**self-exits ~90s** after the browser shows the done screen. (Stopping it the instant you
patch `done` races the browser's poll and freezes the page on "reviewing".) Repo code is
already in the working tree — commit as usual; **never auto-commit, never delete repo
work**. Sandbox dir: leave it, or `rm -rf` only if asked.
## Checkpoints (proactive, opt-in, unobtrusive)
While doing **normal, non-tutoring work**, you MAY *occasionally* offer a tiny hands-on detour
when the user's **actual code** hits a high-value teachable moment. Offer as **one short line,
then STOP and wait** — never auto-start:
> 💡 That `<concept>` is a nice learning moment — want a quick ~5-min hands-on CodeTrain
> detour on it? (yes / no)
**yes** → a 1–2 step micro-session on *that* concept, then return them to their work.
**no** → continue immediately. Hard limits: only at a genuine milestone (never mid-debug);
real learning value only; after one decline, don't re-offer; **soft cap 2/session**; never the
same concept twice; never nag. Opt-in nudge: `app/checkpoint-hook.sh` (off by default) or a
line in the user's CLAUDE.md.
## Cross-agent use & limitations
Mostly agent-agnostic: server + UI are pure `python3` + a browser; all updates go through
`ctl.sh`/files (Bash, Read, Write, python3 — no Claude-only tools). The **auto-wake** (watcher
→ background re-invoke) is a Claude Code nicety; on agents/models without it the loop is
identical except the user **nudges** ("check it") after Send — read the watcher/inbox payload,
then patch. Keep all tutor prose in renderer-supported markdown (`##`, `**bold**`, `` `code` ``,
fenced blocks, `- ` lists). A browser can't read the terminal theme — the UI falls back to
system light/dark + a toggle.