Repository navigation
learn 2.0.0
A security release that also makes the package's own entry points work. It changes where learn
keeps state and how resume authorizes steps; "Moving from 1.6.0" below lists what to do.
Security
Affected: 1.6.0 and earlier.
- Path escape through
sessionIdandrunId.learn_tutor_planjoinedsessionIdinto
tutor/<id>.jsonwith no check, so"../.claude/settings"replaced a project's
.claude/settings.json.learn_tutor_record,learn_verifyandlearn_receiptread or
rewrote files the same way, andlearn_dry_run,learn_tutor_prooflessonand
learn_tutor_reverifyread any path, with a non-JSON file's first characters in the error.
Ids now match[A-Za-z0-9._-]{1,64}, must not start with a dot or a hyphen and must not be a
Windows device name. Every resolved path, after links and junctions, must stay inside the state
folder. Path arguments resolve inside it, and a failure carries a closed code and fixed text. A
path outside the folder on its text, such as a\\host\shareUNC path, is refused before
anything opens it, so no SMB or WebDAV connection is made. A link whose target does not exist
is refused, not followed. - Resume submitted and paid without the opt-in.
learn resumepassed
allowIrreversible: trueon every call. After a halt atassess, a plain resume clicked the
nextsubmitstep and acoststep, and the receipt filed the submit as a witnessed automated
submission. A resume now keeps the run's recorded mode,--submittakes onlymanualor
witnessed-auto, and steps flaggedcostorirreversiblehalt unless--allow-costis given
on the invocation that reaches them. The ledger entry of each step a grant allowed names it,
and the receipt lists it (witnessedAutoSubmissions[].authorizedBy,authorizedCostSteps).
The command line is read once, so a grant word given as the value of another flag, as in
--attest "--allow-cost", is that flag's value and grants nothing. - Children of
LEARN_*_CMDinherited the caller's folder and environment. With the documented
python -m crucible, acrucible/package in the folder wherelearn assist --crucibleran was
executed. Children now start through the vendored safe spawn helper 1.0.1
(src/_vendor/safe_spawn.mjs, pinned inVENDORED.sha256): absolute executable, private empty
folder, environment allowlist extended only byLEARN_CHILD_ENV,
NoDefaultCurrentDirectoryInExePath=1on Windows,-PandPYTHONSAFEPATH=1for Python.
A command given as a bare name is looked up on PATH without any entry that reaches the folder
learn runs in: an entry naming that folder or a folder below it, a junction or symlink to it,
or a quoted spelling of it. The child's PATH leaves those entries out too, so a peer command
that runs a helper by bare name does not find one planted there on PATH. When learn runs in a
filesystem root, in your home folder or in a folder above it, it skips only an entry naming
that folder itself. It never skips Node's own folder or the Windows, System32 and SysWOW64
folders, and when it runs in one of them it skips no entry. On Windows a drive-relative name
such asC:toolis refused. For the same reasonLEARN_NATIVE_CONTROLmust be an absolute
path: a relative value imported abrowser.mjsfrom the folder wherelearn run --native
started. - The shipped
docs/smoke.mdno longer names a local development folder. The code default was
already removed onmainand ships here for the first time.
Breaking changes
- State location. Sessions (
tutor/) and runs (runs/) live inLEARN_HOMEwhen it is set,
else in%LOCALAPPDATA%\learnon Windows,~/Library/Application Support/learnon macOS, and
$XDG_DATA_HOME/learnor~/.local/share/learnelsewhere. 1.6.0 wrote them into the folder the
command started in.learn statusprints the folder understate. - Resume. A plain
learn resumeno longer submits or pays.run --submit witnessed-autono
longer coverscoststeps. A completed or denied run cannot be resumed. - MCP errors. A tool failure is a result with
isError: trueandstructuredContent
{code, retryable, setup, detail}, where code isINVALID_ARGUMENT,NOT_FOUND,CONFLICTor
INTERNAL. 1.6.0 returned JSON-RPC-32000errors with free text. An unknown tool is-32602. - No silent overwrite.
learn_tutor_planandlearn tutor planrefuse to replace an existing
session unlessreplace: trueor--replaceis given. - MCP paths.
learn_dry_runtakesworkflowinline orworkflowPathinside the state
folder;packetPathandfileresolve inside it too.learn_tutor_reverifynames each
receipt relative to the state folder (tutor/<id>.mastery.json), not by its absolute path. - Peer commands. A
LEARN_*_CMDchild sees only allowlisted variables and starts in a private
folder, and a relative path in the command is refused. A peer command installed inside the
folder learn runs in is no longer found by bare name, whether it sits in a project's
node_modules/.binor in a virtual environment inside that folder. With such a venv activated,
pythonresolves to the next Python on PATH, so name the venv's interpreter by absolute path,
for example["/absolute/path/to/course/.venv/bin/python", "-m", "crucible"]. Each peer start
reads every PATH entry, so a slow or unreachable network folder on PATH delays every start. The
interop functionscrucibleAssess,gatherRunandtelosRenderare async; they are not
package exports.
Fixed
- The
learnbin works.src/cli.mjsstarts with#!/usr/bin/env node, the repository checks
out LF everywhere (.gitattributes), and main-module detection compares real paths, so the bins
npm links run. In 1.6.0 the bin printed nothing on Windows and failed on Linux. - The human attestation records the real time of the resume. 1.6.0 recorded 1970-01-01.
- MCP
serverInfo.versionreports the package version; 1.6.0 said 1.0.0. A test now holds
package.json,package-lock.json,src/index.mjs,serverInfo, status, doctor, this file
and the README to one version.
Added
learn mcpand alearn-mcpbin start the MCP server:npx -y @harperz9/learn@2.0.0 mcp.--dir <folder>on the CLI for a project-local state folder, andLEARN_HOMEfor every entry
point.learn statusandlearn_statusreport the folder in use understate, with its
source:--dir,LEARN_HOMEordefault.LEARN_*_CMDalso takes a JSON argv array, which
keeps a path with spaces whole.- A release workflow. On a
v*tag it checks the tag against every version site, smokes the packed
tarball throughnpxon Windows, Linux and macOS, publishes with npm trusted publishing (npm
records provenance), and creates a GitHub Release with the tarball andSHA256SUMS. CI runs the
same tarball smoke on every push. Actions are pinned by commit SHA. - Also released from
mainfor the first time: the repository art and its tests, and the
src/interop.mjsorgan-bundle entries (not exported and not imported by the package).
Moving from 1.6.0
- To keep sessions and runs in a project folder, pass
--dir <that folder>or setLEARN_HOMEto
it. To move them, copy the folder'stutor/andruns/into the folderlearn statusprints. - A run that should submit on resume: pass
--submit witnessed-autoonrunor on that resume. A
run that should pay: pass--allow-coston the invocation that reaches the payment step. - MCP clients that read JSON-RPC error text read
structuredContent.codeinstead. LEARN_TELOS_CMD="node ../telos/src/cli.mjs"becomes
LEARN_TELOS_CMD='["node", "/absolute/path/to/telos/src/cli.mjs"]'. Name any variable a peer CLI
needs inLEARN_CHILD_ENV, for exampleLEARN_CHILD_ENV=ANTHROPIC_API_KEY.