Safety
What a fill exercise does to your files, what stops it
leaving them broken, and how to remove Learner entirely.
The guardrail
A fill exercise deliberately breaks a file you care about, and a
session can end at any moment: a crash, a closed terminal, /clear, a
meeting. So the Stop hook carries a second job. While a leftover marker
survives, it re-blocks the end of the session and asks for the file to be put back
before anything else happens:
🎓 Learner — an unfinished fill-in exercise left // LEARNER-TODO markers in: hooks/learner-config.sh
Before anything else: restore the correct implementation, remove every // LEARNER-TODO it left, and verify it compiles/lints/tests. Do not finish while a marker remains.
Four properties, as they are actually implemented in
hooks/learner-quiz.sh:
- Leftovers only
- A file counts when the working tree has a marker in it and
HEADdoes not. The unit is the file rather than the individual marker: the hook lists the files matching in the working tree, lists the files matching inHEAD, and takes the difference. A marker you committed on purpose is therefore repo content and never triggers it — which is what lets this project write the string in its README, its tests and this very page. In a repository with no commits yet there is noHEADto subtract from, so every marker in the tree counts, which is the right answer there. - Untracked files count
- The search runs with
--untracked. A file the session created a moment ago is the commonest place for a hole to be left behind, and it is precisely the file git would otherwise say nothing about. - It fires even when the quiz is off
- The guardrail runs before the activation check, so neither
"enabled": falsenor a missing or invalidlevelsilences it: an abandoned exercise still has to be cleaned up. Exactly four things stop it — nojq, no session id in the hook payload, not being inside a git repository, or the repository sitting underdisabledPaths. That last one is the deliberate hole: a repository you told Learner to leave alone is left alone, holes included, so if you start an exercise there by hand nothing will chase you about it. - Bounded, so a session can always end
- At most two blocks per outstanding exercise, counted in a per-session file under
$TMPDIR. After the second, the hook goes quiet rather than hang a session on a marker Claude cannot or will not clear — the tree is still broken, but that is your call to make and not a loop you have to escape. Cleaning the tree deletes the counter, so a later exercise starts again with a fresh budget of two. When it does block, it names up to twenty files and says how many more there are.
Worth stating plainly what all of this is not: the guardrail is a check that runs
when a session ends, not a sandbox. It is the reason an interrupted exercise gets
noticed instead of shipped, and the reason fill exercises are confined to
files inside the repository — an edit outside it is never recorded as quiz material, so
a hole can never be cut where a repo-scoped check could not see it. Committing before
you start an exercise still costs you nothing, and git will show you every hole.
What counts as quiz material
An exclusion list, not an include list. Every file Claude edits inside the
repository is material — an edit outside it, a dotfile in $HOME or a
file in another project, is never recorded — minus a built-in floor you cannot
configure away:
- any of these anywhere in the path:
node_modules/,build/,dist/,out/,target/,vendor/,.git/,.gradle/,__pycache__/,.venv/,coverage/,__snapshots__/ - these suffixes:
*.lock,*-lock.*,*.min.*,*.generated.*,*.snap
Then minus your own untrackGlobs on top of that floor. The globs are
matched with shell pattern matching and split on whitespace internally, so
a glob containing a space is not supported.
Uninstall
./uninstall.sh # hooks, skill and wiring; data stays
./uninstall.sh --purge # also delete learner.json and learner/
./uninstall.sh --project /path/to/repo # clean up a legacy per-project install
The plain form is the exact reverse of the install: the six hook files, the skill,
and every learner entry in settings.json, dropping any hook event left
empty. Your config and your progress survive, so a reinstall picks up where you left
off. --purge deletes those too.
--project is for anyone who installed an earlier, per-repository
version of Learner: it removes the hooks, the skill, the .claude/settings.json
entries and the .gitignore lines from inside one specific repository.
Per-repo overrides are not centrally enumerable, so a plain ./uninstall.sh
cannot find them for you — delete a stray learner.local.json by hand, or
point --project at the repo.
Installed via Homebrew or apt? Run learner-uninstall instead of
./uninstall.sh — same flags, learner-uninstall --purge included.
sudo apt remove learner / brew uninstall learner only remove
learner-install and learner-uninstall themselves; neither touches
~/.claude. Run learner-uninstall first if you want that cleaned up
too — do it before the package step if you can, since afterward the binary that does it is
gone.
Both forms refuse to start if settings.json cannot be parsed, and say
so, rather than getting half way and leaving the wiring pointing at hooks that no
longer exist.
Next: what Learner is, if you came in sideways.