A small terminal coding agent. Stdlib-only Python core, minimal context overhead.
Install · Quick start · Providers · Safety · Tools · Context
A local model (Qwen3-Coder:30b via Ollama) auditing a Python package, finding the root cause of a bug, and fixing it.
lean-coder reads, edits, and runs code in your project through a model's native tool-calling API. It keeps the system prompt and tool descriptions short, so more of the context window is left for your code.
Context overhead (system prompt + tool schemas), checked by the test suite:
| Surface | Max tokens |
|---|---|
| Always-on (core tools) | 2500 |
| Every bundled lean-tool enabled | 6500 |
Optional lean-tools are off by default and add nothing until enabled.
- Runs against local models (Ollama, llama.cpp, MLX) or hosted APIs.
- When the context window fills, the agent updates its docs, pins a goal and plan, and continues from a summary (compaction).
- Can dispatch background worker agents for sub-tasks (
dispatch_workerlean-tool). - Runs on Termux, or drives a remote box over SSH with
/connect. - Generic MCP client; no servers are configured by default.
Layout: lean_coder.py (core), lean-tools/builtins.py (required file/shell tools),
lean-tools/ (optional tools), providers/ (model backends). Every change must pass three
test gates; see CONTRIBUTING.md.
Requires python3 3.11+ and curl or git. Minimal images may need these first:
| Platform | Prerequisite install |
|---|---|
| Debian / Ubuntu / WSL | sudo apt update && sudo apt install -y python3 curl |
| Fedora / RHEL | sudo dnf install -y python3 curl |
| Arch | sudo pacman -S --noconfirm python curl |
| Alpine | sudo apk add python3 curl |
| Android / Termux | pkg install -y python curl |
| macOS | python3 + curl ship with the OS (or brew install python) |
Linux / WSL / macOS:
curl -fsSL https://raw.githubusercontent.com/codemonkeying/lean-coder/main/install.sh | bashAndroid / Termux:
pkg install -y python curl && curl -fsSL https://raw.githubusercontent.com/codemonkeying/lean-coder/main/install.sh | bashinstall.sh installs the code and puts lean_coder on your PATH. Add
--with-ollama --pull for a local Ollama on Linux. On Termux, point it at a remote Ollama
(lean_coder --host http://HOST:11434). The installer is idempotent; ./uninstall.sh
removes everything.
Or run from a clone without installing:
git clone https://github.com/codemonkeying/lean-coder
cd lean-coder
./install.sh --dry-run # show what it would do, change nothing
python3 lean_coder.py # local Ollama, default model
python3 lean_coder.py --host http://box:11434 --model qwen3-coder:30bRuntime: Python 3.11+, no third-party packages for the core. Some opt-in lean-tools
have their own deps (e.g. web_screenshot needs Playwright) and say so when enabled.
/connect needs an ssh client. The Anthropic subscription provider needs node.
Updating: re-run the installer, or git pull && ./install.sh. With the update
lean-tool enabled, /update pulls a newer lean_coder.py (update_track = stable or
beta; auto_update = true checks at launch).
lean_coder # uses your config, or localhost Ollama + default modelType a request. The agent shows a diff before each edit and confirms before running
commands (unless approval is session or auto). /help lists commands.
$ lean_coder
lean-coder <your-model> @ <your-provider>
› add a --json flag to the export command and update the tests
● I'll look at the export command first.
⚙ read_file(path=src/export.py)
⚙ search_files(pattern=def export)
...
A provider connects lean-coder to a model backend. Ollama is enabled by default and uses
localhost:11434. Hosted providers ship disabled until you log in:
| Provider | Backend | Get a key |
|---|---|---|
ollama |
Local / self-hosted Ollama (default) | none needed |
anthropic_api |
Anthropic API (Claude) | console.anthropic.com |
gemini |
Google Gemini | aistudio.google.com |
groq |
Groq | console.groq.com |
openai |
OpenAI | platform.openai.com |
openrouter |
OpenRouter | openrouter.ai |
/provider login anthropic_api # prompts for the key, saves it, switches to it
Keys are stored in a chmod 600 file under ~/.config/leancoder/, never in
config.toml. An env var (e.g. ANTHROPIC_API_KEY) is used instead if set. /provider
switches backends; /model lists and switches models across enabled providers.
For another backend, copy examples/providers/example.py
into providers/; see PROVIDER_API.md. Sessions work across providers.
Two independent settings:
/leash- which tools the model gets (defaultrwe):chat(none) ·r(read files) ·rw(+ edit files) ·rwe(+ run commands). The model is told its level./approve- when to confirm (defaultsession):ask(every edit/command) ·session(once per run) ·auto(never).
Examples: leash r for unattended analysis; leash rw + approve auto for unattended
editing. OS file permissions still apply. /incognito writes nothing to disk;
/askread also confirms read tools.
Eight file/shell tools in lean-tools/builtins.py:
| Tool | What it does |
|---|---|
read_file |
Line-numbered file contents; optional line range; large files truncated. |
list_files |
Directory listing, honoring ignore rules. |
search_files |
Regex search -> file:line matches (capped). |
apply_diff |
Preferred edit tool: SEARCH/REPLACE blocks. |
replace_lines |
Replace a line range by number. |
write_file |
Create or overwrite a whole file. |
run_command |
Run a foreground shell command; output truncated. |
background |
Run and manage long-lived tasks (run / status / kill). |
Also always present: update_plan (a pinned goal + TODO that survives compaction),
note (a session notebook), and, by default, ask_user_to_run, which hands a command to
you when it needs sudo, an interactive prompt, or a password. Only the command and exit
code return to the model. Read-only calls in one turn run concurrently.
A lean-tool is one .py file with a TOOL schema and a run function. Bundled ones are
in lean-tools/, off by default; enable them with /tools.
| Lean-tool | Adds |
|---|---|
dispatch_worker |
Run a sub-task in a background worker agent and collect its result. Workers can be steered, restricted to fewer tools, and assigned to a board. With worker_checkpoint on, a stopped worker can be resumed, and pause saves one as a session to continue later. Adds /worker. |
board |
A task board: a dependency graph of tasks the driver assigns to workers. Stored on disk. |
web_fetch |
Read a URL as text. |
web_screenshot |
Screenshot a URL with a headless browser and return the page text. Needs Playwright. |
brave_search |
Web search (Brave API key). |
git_summary |
Read-only git status, diffstat, recent commits. |
diagnostics |
Lint/typecheck with whatever is installed (pyright, ruff, tsc, eslint, shellcheck, …), falling back to py_compile / bash -n. |
symbols |
Outline Python classes/functions or find a definition (stdlib ast). |
shell_session |
A persistent interactive shell (REPL, ssh, …). |
ssh |
One-shot ssh host cmd. |
notify |
Desktop notification when a long task finishes. |
provision |
/provision: install lean-coder on another box over SSH. |
update |
/update: self-update lean_coder.py. |
word_count |
Count lines / words / chars in a file. |
brave_search needs a Brave Search API key in
~/.config/leancoder/brave.key or LEANCODER_BRAVE_KEY. See LEAN_TOOLS.md;
examples/lean-tools/ has templates. There is no LSP integration.
lean-coder is an MCP client. Added servers' tools appear as mcp__<server>__<tool>:
/mcp add fs npx -y @modelcontextprotocol/server-filesystem /some/dir # stdio server
/mcp add gw https://mcp-gateway.example.com/mcp/handbook/mcp # HTTP server
/mcp # enable/disable menu
/mcp list # servers + connection state
/mcp reconnect [name]
/mcp remove <name>
Transports: stdio and HTTP (streamable, SSE or JSON). HTTP auth is a Bearer token, or
OAuth 2.1 client credentials (fetched and refreshed automatically). Configure in
config.toml:
[mcp_servers.gw]
transport = "http"
url = "https://mcp-gateway.example.com/mcp/handbook/mcp"
auth = { type = "bearer", token_env = "GW_KEY" }
# or: auth = { type = "oauth", token_url = "…/oauth/token", client_id = "…", client_secret_env = "GW_SECRET", scope = "mcp:access" }MCP tools run locally (never on a /connect remote), need leash rwe, and confirm like
other non-read tools. See MCP.md.
<<<<<<< SEARCH
exact existing text (must match the file verbatim)
=======
replacement text
>>>>>>> REPLACE
Blocks apply in order. If any SEARCH text isn't found, nothing is written and the model is told to re-read the file.
- Truncation and ignore rules. Large reads and command output are cut head/tail with a
…[truncated N …]…notice..gitignore,.leancoderignore, and built-in defaults (.git/,node_modules/, …) are honored. - Result cap. Every tool result is capped on the way in (head + tail kept), sized to the free window.
/trim [keep]replaces old tool results with one-line stubs, keeping the newestkeep. No LLM call.- Meter. Context use prints after each turn.
/usageshows it;/activitylists automatic actions (compaction, trim, caps).
On /compact, or automatically, the model gets one turn to:
- Update the project's docs (and commit them in a git repo).
- Write a summary: goal, decisions, state, next step.
- Pin a goal + TODO.
- Write the next instruction for itself.
Older history is replaced by the summary (the last compact_keep turns, default 3, stay
verbatim). The next instruction is then sent as a new turn after a 5-second ^C window.
Thresholds: a nudge near ~70% full, forced compaction at compact_at (~90%), emergency
compaction at ~100%. A loop guard limits it to about once a minute. auto_compact,
autostart_after_compact, compact_emergency, and the prompts are configurable.
- Wake on background finish (on by default): a finished background task or worker
starts a turn when the prompt is idle.
wake_on_bg_finish = falsedisables it. - Send window (off by default):
window_messages = Nsends only the last N messages, cut at a turn boundary. For very small models.
Precedence: CLI flag > env var > config file > default.
| Setting | Flag | Env | Default |
|---|---|---|---|
| Ollama endpoint | --host |
OLLAMA_HOST |
http://localhost:11434 |
| Model | --model |
LEANCODER_MODEL |
qwen3-coder:30b |
| Context window | --num-ctx |
- | auto-detect, capped 32768 |
| Project dir | --cwd |
- | current directory |
| Approval mode | --approval / --auto |
- | session (ask once) |
| Capability | --leash |
- | rwe |
| Resume session | --resume <name> |
- | auto-load last for cwd |
Config is ~/.config/leancoder/config.toml and saves automatically on any change. It also
holds host failover, named machines, per-machine default models, and saved /connect
targets. Auto-detect only lowers the context window (capped 32768); use --num-ctx to go
higher.
What stays on disk. Only things you named: config, sessions (~/.config/leancoder/sessions),
and task boards. Everything else a running lean-coder creates (background task logs, worker
files, file-claim boards) lives in one folder per process under $XDG_RUNTIME_DIR (else
$TMPDIR or /tmp). It is removed when that process exits; after a crash the next start on
the same machine removes any folder whose process is gone. A remote executor does the same on
its machine and deletes its copied code when the session closes or the link has been down
longer than LEANCODER_REMOTE_IDLE_TTL (default 30 min).
/clear wipe conversation, stay in this session
/new [name] start a separate session
/trim [keep] stub old tool outputs, keep newest [keep] in full (no LLM)
/compact [k|to] agent commits durable docs, writes a future-self summary, replaces history
(k = compact to ~k thousand tokens this once; `to` prompts for it)
/save [name] name the current session
/load [name] resume a session (no arg = picker)
/session list | delete <name>
/prompt [name] view/edit prompt files (/prompt use <name> = fire one as a turn)
/sh [cmd] run a command yourself in a terminal (no arg = your $SHELL)
/connect [host] run tools on a remote box over SSH (no arg = pick saved/open)
/local [host] detach the active remote (keep it open to switch back)
/machines manage saved remote hosts (list/add/remove)
/tools enable/disable lean-tools
/mcp manage MCP servers (add/remove/reconnect; no arg = enable/disable menu)
/reload reload lean-tools + pick up prompt edits
/model [name] switch model across enabled providers (no arg = list)
/provider [name] switch/manage the model provider
/usage session tokens + context / provider usage
/think [level] set thinking level (no arg = menu)
/effort [level] set reasoning effort (no arg = menu)
/set [key val] edit app config (config.toml knobs; no arg = menu)
/provider set [k v] get/set a backend-specific provider knob
/approve [mode] confirm cadence: ask | session | auto
/leash [level] capability ceiling: chat | r | rw | rwe
/autosave [on|off] autosave + auto-load last on start
/incognito [on|off]don't save the session locally
/askread [on|off] confirm read tools too
/bg [kill <pid>] list/kill background tasks
/info live session read-out
/activity [n|all] what the system did automatically (compaction, trim, fallback, …)
/expand [N] show a tool call's full (untruncated) args
/help list commands
/quit exit
/<cmd> ? shows help for a command. Tab completion and history work when readline is
available; menus are arrow-key navigable.
/prompt edits the built-in prompts (system, compact, auto_compact,
compact_nudge; /prompt reset <name> reverts). /prompt new <name> creates your own,
and /prompt use <name> sends one as a turn.
/connect <[user@]host> [path] runs all file and shell tools on a remote box over SSH.
The prompt shows [remote: host] ›.
- The remote only needs
python3; the script is copied over (skipped if unchanged). - The remote executor has no config, model, secrets, or network access; it only runs approved tool calls in that directory.
- Previews and confirmations happen locally; remote edits show a real diff.
- SSH authenticates once per connection; its output never enters the model's context.
The session stays the same when switching between local and remote.
- Send
[system] + history + latest turnwith the tool list. - Stream the reply.
- Run any tool calls, append results, repeat.
- A reply with no tool calls ends the turn.
- Tool rounds per turn are capped.
A batch of only read-only calls runs concurrently; any batch with a write or command runs in order, as does everything on a remote.
Three gates, from the repo root (each exits non-zero on failure):
python3 tests/_smoketest.py # unit suite (incl. the overhead limits in this README)
python3 tests/_mocktest.py # scripted end-to-end suite
bash tests/_sweep.sh # hygiene lint (stray unicode, likely secrets/PII, etc.)See CONTRIBUTING.md, LEAN_TOOLS.md, and PROVIDER_API.md.
MIT.
