Configuration
Every setting Learner reads, where to put it, and how to make it stop.
Two layers, read in this order:
$CLAUDE_CONFIG_DIR/learner.json(default~/.claude/learner.json) — your defaults for every repository. Written by the installer, edited bylearner config ….<repo>/.claude/learner.local.json— optional, gitignored, a partial override for one repository. Written bylearner off,learner onandlearner config project ….
The project layer wins key by key, and arrays are replaced
wholesale rather than merged: a project untrackGlobs is the whole list for
that repo, not an addition to the global one. A key you did not set keeps its global
value, and "enabled": false survives the merge — it is a deliberate
override, not an absent key.
| Key | Values | Default | Effect |
|---|---|---|---|
level | D/J/C/S/E | — required | How hard a question is. No valid level means no questions |
enabled | bool | true | Master switch for the automatic quiz |
questionStyles | "auto", or a subset of code/architecture/fill | "auto" | Which formats a question may take |
synthesisFrequency | off/rare/normal/often | normal | A synthesis question every 0 / 8 / 4 / 2 questions |
blanksPerExercise | int ≥ 1 | 2 | // LEARNER-TODO holes cut in a fill exercise |
untrackGlobs | array of globs | [] | Extra paths excluded from quiz material |
disabledPaths | array of path prefixes | [] | Repositories where Learner stays silent |
level is the only one of the seven with no default. The other six have
theirs in LEARNER_DEFAULTS at the top of
hooks/learner-config.sh; level is left out on purpose, so a
half-finished install asks nothing rather than guessing how much you know.
SessionStart mentions it once when that happens. A full config looks like
this:
{
"level": "C",
"enabled": true,
"questionStyles": "auto",
"synthesisFrequency": "normal",
"blanksPerExercise": 2,
"untrackGlobs": ["*.md", "*.json"],
"disabledPaths": []
}
You do not have to edit it by hand: learner config level=S validates
and writes for you, and learner config project synthesisFrequency=often
does the same for the current repository only. The same shape ships in the repository
as learner.json.example, for reference or copy-paste.
Levels
The canonical value is the letter. The full word is accepted too, in any case, so
senior, Senior and S are the same setting.
| Letter | Name | What a question targets |
|---|---|---|
D | Discovering | syntax, what a block is for, basic vocabulary |
J | Junior | what the function does, where the code lives |
C | Competent | why this split, edge cases, error handling |
S | Senior | trade-offs, rejected alternatives, impact on performance and coupling |
E | Expert | invariants, failure modes, what breaks at scale |
The level is a claim about you on this code, not a job title, and it is one word to change. Set it high and the easy questions stop; set it low for a stack you have never touched. Anything that is not one of the five letters or names is not a level at all: the hook treats it as unset and asks nothing.
Turning it off
Three ways, by scope. The first two leave the guardrail in place; the third silences that too, which is rather the point of it.
- Everywhere. Set
"enabled": falsein the global config, or saylearner config enabled=false. An explicitlearner quizstill runs when you ask for one — you asked. - One repository you own. Say
learner offin it. That writes<repo>/.claude/learner.local.jsonand adds the file to the repo's.gitignoreif it is not already there.learner onreverses it. That file is the only thing Learner ever adds to a repository, and only on request — afillexercise (see above) is a different kind of change: it temporarily edits one of your own source files rather than adding a new one. - One repository you do not own, where nothing of yours should be
committed. Add its path to
disabledPathsin the global config —learner config disabledPaths='["/path/to/repo"]'. Nothing is written into that repository at all. Matching is a path-prefix check on whole components, so disabling/a/bsilences/a/b/cbut not a sibling/a/bee; a leading~/is expanded, and an entry that exists on disk is resolved to its physical path first, so an entry that reaches the repo through a symlink still matches.
Next: Safety — what a fill
exercise does to your files.