v1.12.0
Fixed
-
little-coder no longer ships an npm install script, so Socket's malware scanner has nothing to flag (#75 by @modemlooper, diagnosed as a false positive by @abhisek).
npm install -g little-coderwas producingPotential malware detected with AI scanfrom Socket Firewall, pointing at thepostinstallhook. The hook ranscripts/patch-pi.mjs— a visible, dependency-free file in the repo that re-applies two cosmetic source edits to the bundled pi — so the alert was a false positive on the shape of the code (an install script touchingnode_modules) rather than on anything it did. Rather than just explain that, v1.12.0 removes thepostinstallentry entirely: the launcher already callsapplyPiPatches()on every launch, and every self-updating user was skipping the postinstall anyway because/updateand the launcher's auto-update both install with--ignore-scripts(#50). Launch-time patching is now the only path, which also means it self-heals if pi is reinstalled underneath.scripts/patch-pi.mjsitself is unchanged in behavior and still shipped — it's imported by the launcher, no longer executed by npm. -
The launcher's pi patching and pi-changelog suppression were both silently dead (found while investigating the above; no issue).
bin/little-coder.mjsresolved pi's package root inside afor (const piPkgRoot of piPkgCandidates)loop and then referencedpiPkgRoottwice further down — at the patch call and at thelastChangelogVersionpin. Afor (const …)binding is scoped to the loop body, so both references threwReferenceError, and because both sit inside deliberately best-efforttry/catchblocks the failure was completely invisible. Two real consequences: our pi runtime patches never re-applied at launch (so a--ignore-scriptsupgrade left pi unpatched), andlastChangelogVersionwas never written — which meant pi's own upstream "What's New" changelog rendered inside the little-coder TUI after every bundled-pi bump. The binding is now declared at module scope and assigned on the successful candidate. Two end-to-end regression tests (bin/launcher-pi-root.test.mjs) run the real launcher and assert both effects land; both fail against the old code. -
ShellSessionno longer routes around the write guard and the permission gate (#70 by @rvanswieten). Qwen3.6-35B-A3B, refused a whole-filewrite, switched tocat > backend/main.py << 'ENDOFFILE'and got the same bytes anyway — 5 times in one session (main.py×3,App.jsx,pyproject.toml). rvanswieten's diagnosis was correct on all three counts:write-guardmatched onlytoolName === "write",permission-gatematched onlybash/Bash, andShellSessionhit neither, landing straight inexecSync. The bash whitelist wouldn't have saved it either —isSafeBashwasstartsWithon the raw string andcatis whitelisted, with no awareness of the>immediately after it. Fixed with a shared, pure command analyzer (_shared/shell-write.ts) that strips heredoc bodies (so a>or an apostrophe in the payload can't confuse it) and reports the paths a command writes to via>,>>,tee, ordd of=, ignoring fd duplication (2>&1,>&2), process substitution, input redirects, and anything inside quotes. Three behavior changes follow:ShellSessionis gated exactly likebash; every command in a&&/||/;/|chain must pass the whitelist independently (ls && rm -rf /is refused on therm, not admitted on thels); and any command that writes through the shell is refused inauto/manualmode, with a message naming the path and pointing atWrite/Edit. Inaccept-allmode (benchmark runs) the whitelist is still skipped, butwrite-guardnow inspects shell writes too, so a redirect that would clobber an existing file gets the same Edit recipe thewritetool gives, and a reserved Windows device name (#60) is refused even as an append. A>>append to an existing file is allowed — nothing is destroyed, so it isn't the whole-file-rewrite failure mode the guard exists to prevent. Deliberately not matched on the heredoc delimiter string, which any other delimiter would have defeated. -
Per-turn skill and knowledge injection no longer destroys the KV cache (#73 by @manueloverride, with @charly1r). manueloverride caught this with
cache-hunter: llama.cpp suddenly re-churning 120k of message history "for no reason" mid-conversation, because the harness had changed the beginning of the prompt. Exactly right.skill-injectandknowledge-injectappended their selected blocks to the system prompt, which is the first thing in every request — and since both blocks are recomputed per turn from the user's prompt, they changed on most turns and invalidated the entire cached prefix each time. The fix uses a hook pi already had:before_agent_startmay return amessageinstead of asystemPrompt, which pi appends after the user's message and converts to auser-role message on the way to the provider. The guidance now lands at the tail of the conversation with every preceding byte untouched, so the prefix stays cached and only the new tokens are processed. The recency argument that put these blocks last in the system prompt gets stronger rather than weaker — the conversation tail is as late as placement gets. All four injectors moved onto one shared helper (_shared/inject.ts):skill-inject,knowledge-inject,plan-mode's synthesis instructions, anddeep-research's report brief (the largest of the four, and the one where a system-prompt rewrite cost the most). Blocks are hidden from the transcript (display: false), and a block identical to the previous turn's is not re-sent — the earlier copy is still in the conversation, so repeating it would only spend context.LITTLE_CODER_INJECT_MODE=systemrestores the old placement, which is what the whitepaper scaffold reproduction was measured against. A regression fence now fails the build if any extension returns a rewritten system prompt directly, since the symptom is invisible from inside little-coder.Measured against a live llama.cpp server (Qwen3.6-35B-A3B, 4 turns, same prompts in both modes, counting the prompt tokens llama.cpp actually evaluated per request):
Turn context old: system prompt new: tail message 1 ~10.6k 6,525 6,529 2 ~16.5k 12,311 6,235 3 ~23k 18,488 6,377 4 ~29k 24,586 6,438 total 61,910 25,579 The old numbers climb with the conversation — each turn re-evaluates almost everything, which at manueloverride's 120k context is the 120k re-churn he reported. The new numbers are flat: only the genuinely new tokens are evaluated, whatever the history length. 58.7% fewer prompt tokens over four turns, and the gap widens with every additional turn.
Note for anyone who arrived from the same thread: the llama.cpp/Qwen KV-cache bug charly1r linked is a genuinely separate problem, upstream of little-coder.
-
The startup header no longer advertises a key that does nothing (#74 by @heinrichI). The header's hint row listed
ctrl-r more, but pi binds "expand / more" toapp.tools.expand=ctrl+o;ctrl+ris bound only inside the session-tree overlay, so at the prompt it genuinely did nothing. Corrected toctrl-o, andf2(deep research) was missing from both the header and thectrl+hshortcuts panel — the one flow people ask about was absent from the panel whose whole job is discoverability. Both now list it, andbuildHeaderis covered by tests that assert every advertised key is a real binding. Thectrl+ohalf of the report is expected behavior rather than a bug, now documented: during a Deep Research run the research sub-coders are separate child processes, so their tool output never enters this session's transcript and there is nothing forctrl+oto expand — the progress bar is the view of that work — and while the max-agents or clarifying-question dialogs are open, keys belong to the dialog. -
Two panels were silently losing their last rows to pi's widget height cap (found while verifying this release against a live TUI; no issue). pi renders a string-array widget through
content.slice(0, MAX_WIDGET_LINES)— the cap is 10 — and appends... (widget truncated), so anything past ten lines is dropped from the end. Two places were over it: thectrl+hshortcuts panel had grown to eleven rows plus a header, which quietly ate/hotkeys— the row whose entire job is pointing at the authoritative keybinding reference — so it now lays out in two columns and shows every shortcut in seven lines; and the sub-coder tracker appends a row per sub-coder inbegin()and never resets, so a session with severaldispatchturns accumulated rows without bound and pi's truncation then hid the sub-coders that were actually running behind a backlog of finished ones — exactly backwards for a live progress view. The tracker now always shows every running sub-coder, fills the remaining rows with the most recently finished, and accounts for the rest on one… +N earlier sub-codersline, with the header still reporting the true total. Both are covered by tests that assert the ten-line ceiling.
Added
- A first-class, opt-in way to add your own extensions (#67 by @thegausiantheory; #69 by @johnzan, with @charly1r). little-coder launches pi with
--no-extensionsand loads exactly its bundled set, which is what keeps the cold-start context near 7k tokens and makes behavior predictable — charly1r's write-up on #69 explains the tradeoff better than the docs did. But "I want to add my own" is a fair ask, and the only answer was an env var nobody found (LITTLE_CODER_EXTRA_EXTENSIONS) or forking the installed package (#46). Three additions, all opt-in, all no-ops when unused:- A user extension directory.
~/.config/little-coder/extensions/(or$XDG_CONFIG_HOME/…, orLITTLE_CODER_EXTENSIONS_DIR). Each direct child is one extension — a.ts/.js/.mjsfile, or a directory with anindex.ts/index.js. Loaded after the bundled set, so yours can override ours. The directory is never created for you and nothing is written to it; an install that ignores it behaves exactly as before. Survivesnpm install -g little-coder@latest. /extensions— a panel showing what's loaded, split by where it came from (bundled / yours /LITTLE_CODER_EXTRA_EXTENSIONS), whether pi's own discovery is on, and anything that failed to load. Because little-coder forcesquietStartupto keep launch clean,--verboseused to be the only window into this, and a user extension that didn't resolve warned once on stderr moments before the TUI painted over it — those warnings now also fire as a notification at session start. Rendered as a widget rather than a chat message on purpose: a custom message would be sent to the model, spending context on a diagnostic it has no use for.--with-pi-extensions(orLITTLE_CODER_PI_EXTENSIONS=1) — omits--no-extensionsso pi discovers its own extensions from~/.pi/agent/extensionsand./.pi/extensions. Off by default and it prints a notice when on, because the guarantee it gives up is real: the extension set is no longer fixed, cold-start context grows, and a cloned repository can contribute extensions from its.pi/directory (pi's own trust prompt still gates that code).
- A user extension directory.
- New guide:
docs/extensions.md— how to write an extension, where to put it, which bundled extension to read as a reference for each kind of change, the two things to know before writing one (don't rewrite the system prompt per turn; cap rendered lines to the terminal width), and a community list: BMorgan1296's Zed/pi-acpbridge (#58), johnzan's Telegram bridge (#69), and charly1r's llama.cpp configs and local-model tournament results (#63). Community projects are linked, not vendored.
Docs
- The status line is documented (#71 by @johnzan, first answered by @charly1r). A new README section explains every field, read off pi's
footer.jsrather than inferred. Three corrections to the reasonable-looking guess in the thread: the first number is cumulative session input tokens, not current context size;CHis the cache-hit rate of the latest response alone (cacheRead / (input + cacheRead + cacheWrite)), not a session average, which is why it swings; and(auto)specifically means automatic compaction is enabled. A lowCHon a long conversation is a real signal — it's exactly what #73 above was causing. - Three stale README facts corrected. Plan Mode is
ctrl+q, notalt+p(it moved in v1.9.0 and the README never followed). Sub-coder concurrency defaults to 1, serial, not 2 (changed in v1.9.10 by #57). And the claim that pi themes don't load was wrong:--no-extensionsgates extensions only — pi's theme discovery is governed by a separate--no-themesflag that little-coder never passes, so pi themes in~/.pi/agent/themesand./.pi/themeshave always worked. That correction also applies to my earlier answer on #67. - New Troubleshooting entries for the malware alert,
ctrl+r, pi extensions and themes, extension load failures, and the llama.cpp reprocessing symptom.