# locode An agentic coding CLI, in the spirit of Claude Code, for models running locally via [Ollama](https://ollama.com) or [LM Studio](https://lmstudio.ai). It talks to either backend's OpenAI-compatible `/v1/chat/completions` endpoint, so any model you can serve from either one works here. ## Install ```sh npm install npm run build npm link # makes the `locode` command available globally ``` ## Quick start Make sure Ollama (`ollama serve`, default `http://localhost:11434`) or LM Studio (with a model loaded, default `http://localhost:1234`) is running, then: ```sh locode --model qwen3-coder:30b ``` If you omit `--model`, locode lists the models available from the backend and lets you pick one with the arrow keys (Enter to confirm). Your saved default (via config or `$LOCODE_MODEL`) is pre-highlighted. Pass `--model` explicitly to skip the picker entirely. Or persist your defaults so you don't need flags every time: ```sh locode config set backend ollama locode config set model qwen3-coder:30b locode ``` List models available from the configured backend: ```sh locode models ``` Every conversation is auto-saved as you go. Resume later: ```sh locode --continue # resume the most recent conversation locode --resume # pick from a list of saved conversations locode --resume # resume a specific one locode sessions list # see saved conversations (id, model, title) without starting the UI locode sessions rm # delete a saved conversation ``` locode can also use tools from external [MCP](https://modelcontextprotocol.io) servers: ```sh locode mcp add my-server --command npx --arg -y --arg @some/mcp-server # stdio server locode mcp add my-remote --url https://example.com/mcp # remote (streamable HTTP) server locode mcp list # see configured servers (user-level + project .mcp.json) locode mcp remove ``` Project-level servers can also be checked into a repo via a `.mcp.json` file in its root: ```json { "mcpServers": { "my-server": { "command": "npx", "args": ["-y", "@some/mcp-server"] } } } ``` locode connects to every configured server on startup; their tools show up alongside the built-in ones, namespaced as `mcp____`. locode can also install **Claude Code plugins directly** — the parts that map onto locode's own architecture, anyway (see below for what doesn't): ```sh locode plugin add ./path/to/plugin # a local plugin directory locode plugin add https://github.com/someone/some-plugin.git # cloned via git locode plugin list locode plugin remove ``` Hooks (see below) are hand-edited JSON rather than added via a CLI subcommand: ```sh locode hooks path # print the user-level hooks.json path to edit locode hooks list # see every configured hook (plugin + user + project), merged ``` ## How it works locode is a full-screen terminal app built with [Ink](https://github.com/vadimdemedes/ink) (the same React-for-CLI framework Claude Code itself is built with) — it needs a real interactive terminal (piped/redirected input isn't supported). It takes over the terminal's alternate screen buffer (like `vim`/`htop`) — your prior scrollback is restored when you exit. The input box is always pinned to the last row of the window; the conversation fills the space above it and old messages scroll off the top as new ones arrive. Press Ctrl+C or type `/exit` to quit. - **Ctrl+O**: print the full text of the last `/compact` (or auto-compact) summary. The collapsed notice you see right after compacting only shows a one-line hint — press Ctrl+O any time afterward to print the whole thing. - **Ctrl+B**: while a `bash` command is running, detaches it into the background and returns control to you immediately — the turn continues with a `bash_output`-checkable job id instead of waiting for the command to finish. A notice appears in the transcript once the backgrounded command actually completes. Only `bash` supports this today. The model can kill a still-running backgrounded job with `bash_kill`; any jobs still running when locode itself exits are killed too, so they don't outlive the process as orphans. - **Ctrl+F**: open and focus a file panel docked to the right of the chat (hidden by default); press again to close it. It has two tabs — **Files**, the project's collapsible file tree (directories in cyan, same `node_modules`/`.git`/`dist` exclusions as `@` mentions), and **Activity**, the files `read_file`/`write_file`/`edit_file` have touched so far this session, most recent first, with a status glyph (`·` read, `+` written, `~` edited) and a repeat count. **Ctrl+G** switches between the two tabs. While the panel is focused, `↑`/`↓` move the selection (auto-scrolling to keep it in view), `↵`/`←`/`→` expand or collapse the selected folder, and **Esc** hands keyboard focus back to the chat input without closing the panel — typing is disabled while the panel has focus, so the same arrow key doesn't simultaneously recall chat history. - **Backends**: `--backend ollama` (default) or `--backend lmstudio`, or `--base-url ` for anything else that speaks the same API. - **Tools**: `read_file`, `list_files`, `grep`, `definition`, `references`, `diagnostics`, `web_search`, `web_fetch`, `git_status`, `bash_output`, `todo_write`, `task_create`, `task_list`, `task_get`, `task_update` run automatically. `write_file`, `edit_file`, `multi_edit`, `bash`, `bash_kill`, and `git_commit` show a diff/preview in a bordered box and ask you to pick Yes / Yes-always-this-session / No with the arrow keys before running. - **Permission modes**: `default` (ask before every mutating tool), `plan` (research only — every mutating tool is blocked outright, no prompt; the model is expected to describe what it would do in its final answer instead), `auto-edit` (file edits auto-approved, `bash`/`git_commit` still ask), `auto-accept` (everything auto-approved — use with care). Cycle with `Shift+Tab` or set directly with `/perm `. - **Structured tasks**: for multi-step work the model can call `task_create`/`task_list`/`task_get`/`task_update` to track units of work with a dependency graph (`blocks`/`blockedBy`), ownership (`owner`), status, and free-form metadata — created incrementally rather than replaced wholesale. The older flat `todo_write` live checklist (`☐`/`◐`/`☑`) remains for simpler cases. - **Project instructions**: a `CLAUDE.md` (or `AGENTS.md`) file in the project root is automatically read at session start and folded into the system prompt — put repo-specific conventions there and every session picks them up without being told. - **Images**: `read_file` returns image files (png, jpg, jpeg, gif, webp, bmp — up to 5MB) as actual image content instead of trying to decode them as text, so vision-capable models can see them when the model itself calls the tool. To attach a file or image to your own message directly, use `/import [caption]`. - **`@` file mentions**: type `@` in the chat input to open a fuzzy file picker (searches the whole project, skipping `node_modules`/`.git`/`dist`) — keep typing to filter, `↑`/`↓` to navigate, `Tab` (or `Enter`) to insert the highlighted path. Any `@path` left in your message when you hit `Enter` for real is resolved against disk and attached to that message (text inlined, images attached as image content) — a stray `@` that isn't an actual file (e.g. an email address) is left as plain text. - **Git**: `git_status` covers read-only inspection (`status`, `diff`, `log`, `show`, `branches`) and runs automatically. `git_commit` covers `add`, `commit`, `create_branch`, `checkout`, and `push` — each shows the actual diff/status/commits it's about to affect before you confirm (e.g. a commit's preview is the staged diff plus the message, a push's preview is the list of commits it would send). - **Sub-agents**: the `agent` tool lets the model delegate a self-contained task to a fresh, isolated tool loop (same tools, minus `agent` itself — no nested sub-agents) and get back only the final answer, keeping the main conversation's context focused. It shows up as a single `⏺ Agent(description)` / `⎿ Sub-agent finished (...)` line — the sub-agent's own intermediate steps aren't displayed. Any mutating tool calls it makes still go through the same permission prompts as the main conversation. - **MCP servers**: locode connects to any [MCP](https://modelcontextprotocol.io) servers configured via `locode mcp add` or a project's `.mcp.json` (stdio and remote/streamable-HTTP transports), and adds their tools to every session, namespaced as `mcp____`. Every MCP tool is treated as mutating (confirmation required on every call) regardless of what it reports — the MCP `readOnlyHint` annotation is advisory and could be wrong (or set by a malicious server specifically to skip confirmation), so locode never trusts it. One misconfigured server doesn't block the others — check `/mcp` for per-server connection status. - **Claude Code plugins**: `locode plugin add ` installs a Claude Code-compatible plugin — locode reads its `.claude-plugin/plugin.json`, then loads it all directly: MCP servers (its `.mcp.json` or manifest `mcpServers`, merged in like any other MCP server), slash commands (`commands/*.md` — frontmatter `description`/`argument-hint`, body is a template expanded with `$ARGUMENTS`/`$1..$9` and submitted as your message), agents (`agents/*.md` — the body becomes a sub-agent's system prompt, exposed as a callable tool named `agent____`; a `tools:` frontmatter list restricts what it can use, with Claude Code's built-in tool names — Read, Grep, Edit, etc. — automatically mapped to locode's equivalents), hooks (`hooks/hooks.json`, see below), and skills (`skills/*/SKILL.md`, see below). Check `/plugins` for what's loaded. - **Skills**: named instructions the model loads on demand rather than a hook or a sub-agent — every installed skill (`skills//SKILL.md`) is exposed through one shared `skill` tool, whose own description lists every skill's name and "use this when..." blurb so the model knows when to call it. You can also invoke one directly with `/ [request]`, which skips the model's own judgment and submits the skill's instructions (plus your request, if any) as the turn. Check `/skills` for what's installed. - **Hooks**: shell commands that fire on session lifecycle events — `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PermissionRequest`, `SubagentStart`, `SubagentStop`, `CwdChanged`, `FileChanged`, `ConfigChange`, `Stop`, `SessionEnd`. Configured the same way MCP servers are — plugin-bundled (`hooks/hooks.json`), user-level (`locode hooks path`, hand-edited), and project-level (`.locode/hooks.json`) all merge together, every hook from every source runs. A hook receives a JSON payload on stdin (`session_id`, `cwd`, `hook_event_name`, plus event-specific fields like `prompt` or `tool_name`/`tool_input`); exit 0 allows (stdout becomes injected context for `SessionStart`/`UserPromptSubmit`), exit 2 blocks (stderr is the reason shown), anything else is a non-blocking warning. `PreToolUse`, `UserPromptSubmit`, and `PermissionRequest` can block; the rest are fire-and-forget. `PreToolUse` fires before permission modes apply, so a hook's block can't be bypassed by auto-accept. `command` and `http` hook types run an external process/request; a `prompt` hook just injects its static `message` as additional context instead. Command/http hooks can opt into structured JSON output via `outputSchema: "json"`. `SessionEnd` fires on every exit path (including Ctrl+C and external `SIGTERM`/`SIGHUP`). Check `/hooks` for what's configured. - **Tool-calling mode**: on connect, locode probes whether the model reliably uses native OpenAI-style function calling. If not, it switches to a prompt-based fallback mode where the model is instructed to emit tool calls as fenced ` ```tool_call ``` ` JSON blocks, which locode parses itself. The result is cached per backend+model so future sessions skip the probe. Override with `--tool-mode native|fallback|auto` or the in-session `/mode` command. - **Context tracking & compaction**: the status bar shows `ctx NN%` — context window usage, from real `usage.prompt_tokens` when the backend reports it (requested via `stream_options.include_usage`), or a `~`-prefixed char-based estimate otherwise. The window size itself is auto-detected (Ollama's `/api/show`, then LM Studio's `/api/v0/models`) and cached per backend+model; falls back to a configurable default (`locode config set contextWindow `, or `$LOCODE_CONTEXT_WINDOW`) if neither responds. At 85% usage, locode automatically asks the model to summarize the conversation and replaces the history with that summary (a notice tells you when this happens) — or trigger it yourself anytime with `/compact`. - **Code intelligence (LSP)**: `definition`, `references`, and `diagnostics` use a real language server (LSP) for go-to-definition, find-all-references, and type/syntax error checks — the same engine an editor's Problems panel uses, more precise than `grep`. A server is lazily started per language on first use and reused for the whole session: `typescript-language-server` (TypeScript/JavaScript), `pyright-langserver` (Python), `gopls` (Go), `rust-analyzer` (Rust), and `clangd` (C/C++ — one clangd covers both). The relevant server binary must be on your PATH; if it isn't, the tool returns a clear "install X" error. After any edit, locode syncs the file to the live server so a subsequent `diagnostics` call reflects the change (it waits for the server to publish fresh diagnostics rather than reading a stale snapshot). Add or override servers with `locode config set lspServers` (see Config). Note: even models with genuine native tool-calling support occasionally emit a tool call as plain text instead of a real structured call — this is model sampling variance, not a bug. If a turn seems to "describe" a tool call instead of running it, just ask again or try `/mode fallback`. ## Slash commands ``` /model switch the model used for the current backend /backend switch backend (ollama | lmstudio), keeps current model /mode view or force tool-call mode (native | fallback) /mouse [on|off] toggle mouse tracking (on by default; hold Shift+click/drag for native text selection) /perm [mode] cycle or set permission mode (default | plan | auto-edit | auto-accept) /status show current model, backend, tool-call mode, and cwd /dashboard show session stats: token I/O, elapsed/model time, turns, tool calls /tools list available tools /permissions list mutating tools allowed for the rest of this session /sessions list saved conversations you can resume with --resume /mcp show connected MCP servers and their tool counts /plugins show installed Claude Code-compatible plugins (commands, agents, skills, MCP servers) /hooks show configured hooks per lifecycle event /skills show installed skills; / [request] invokes one directly /compact summarize the conversation now to free up context /export [file] save the conversation as markdown (or /export json [file] for a full JSON dump incl. tool calls/results) /import [caption] attach a local file or image to your next message /clear clear conversation history /help show this help /exit, /quit exit ``` ## Config Config precedence: CLI flags > env vars (`LOCODE_BACKEND`, `LOCODE_MODEL`, `LOCODE_BASE_URL`, `LOCODE_CONTEXT_WINDOW`, `LOCODE_MAX_ITERATIONS`, `LOCODE_AUTO_COMPACT_THRESHOLD`, `LOCODE_REQUEST_TIMEOUT_MS`) > persisted config file > defaults. ```sh locode config set backend ollama locode config set model qwen3-coder:30b locode config set contextWindow 32768 # fallback size when auto-detection fails locode config set maxIterations 40 # max tool calls per turn before locode gives up (default 50) locode config set autoCompactThreshold 0.85 # fraction of context window at which auto-compact triggers locode config set requestTimeoutMs 300000 # per-request timeout in ms (default 180000); raise this if # your backend queues requests behind a concurrency limit # (e.g. Ollama's OLLAMA_NUM_PARALLEL) under multi-session load locode config set lspServers '{"java":{"command":"jdtls","extensions":[".java"]}}' # add a language server # (JSON object keyed by language id; built-in ids # override command/args, new ids add support and # require extensions). Built-ins: typescript, # python, go, rust, c (C/C++ share clangd). locode config get locode config path ``` ## Known limitations - Requires a real interactive terminal (TTY) — you can't pipe input into it or run it from a non-interactive script. - Native tool-calling reliability varies by model and is non-deterministic even for capable models (see above). - No OS-level sandboxing (no container/VM isolation) — mutating tools operate on the real filesystem/shell with the permissions of the user running `locode`. Only approve commands you understand. Two lightweight guardrails run unconditionally regardless of permission mode (including `auto-accept`), as a safety floor rather than a full sandbox: `write_file`/`edit_file`/`bash`'s `cwd` override can't target a path outside the working directory (`../` traversal, an absolute path elsewhere, or — on Windows — a different drive all refuse), and `bash` refuses a short list of unambiguously catastrophic commands (wiping the filesystem root or home directory, a fork bomb, formatting/wiping a whole drive, writing raw data to a block device) before they'd ever run. Neither guard stops a model from doing damage confined to *within* the project directory, or running something merely inadvisable — see `src/tools/pathGuard.ts` and `src/tools/bashGuard.ts`. - In-app scrollback: mouse wheel scrolls the conversation view when mouse tracking is on (the default). PageUp/PageDown also scroll a page at a time. Hold Shift+click/drag for native terminal text selection and copy (when mouse tracking is on). Scrolling back up unpins the view from the latest message; scrolling back to the bottom (or sending a new message) re-pins it so new messages auto-scroll into view. Toggle mouse tracking with `/mouse on|off`. - Windows shell quoting for the `bash` tool has only had light testing; behavior may differ from Unix shells for complex quoting. - `git_commit` covers add/commit/create_branch/checkout/push/reset/stash/merge/rebase/delete_branch. Use `bash` for anything beyond that. - MCP tool results support text, image, audio, and resource content blocks. Images are returned in the same shape as `read_file` so vision-capable models can see them; audio and binary resources are summarized. Remote (HTTP) MCP servers support static headers (e.g. a bearer token) but not OAuth flows. - Compaction (`/compact` or automatic) replaces history with a model-generated prose summary — it costs one extra model call and loses tool-call/tool-result detail (the model's own account of what happened survives; the raw record doesn't). The auto-compact threshold defaults to 85% and is configurable via `locode config set autoCompactThreshold` or `LOCODE_AUTO_COMPACT_THRESHOLD`. - Plugin support (`locode plugin add`) now covers every part of a plugin: MCP servers, slash commands, agents, hooks, and skills. A slash command's `allowed-tools` frontmatter restricts that one invocation's toolset (same tool-name translation as an agent's `tools:` — see `/plugins`); a skill invoked directly via `/` isn't restricted this way, since skills have no `allowed-tools` field of their own. Duplicate MCP server names across sources are now detected and surfaced in `/mcp` — project-level wins over user-level wins over plugin-level. Duplicate slash-command and skill names are also surfaced in `/plugins` and `/skills`. - Skills are exposed as one shared `skill` tool rather than one tool per skill — if two plugins install a skill with the same name, the first plugin in load order wins and the collision is shown in `/skills`. Skills can bundle sibling `references/*.md` files that are included when the skill is invoked. - Hooks cover the most useful subset of Claude Code's lifecycle events: `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PermissionRequest`, `SubagentStart`, `SubagentStop`, `CwdChanged`, `FileChanged`, `ConfigChange`, `Stop`, `SessionEnd`. `CwdChanged` is declared in the config format but not yet fired anywhere — a cwd never changes mid-session in locode today, so configuring it is a no-op for now. All three hook types are supported: `command` and `http` run an external process/request, and `prompt` just injects its static `message` as additional context (the same way a command/http hook's stdout does) — it has no process to fail, so it can't block an event the way a command hook's exit code 2 can. Command/http hooks can opt into structured JSON output via `outputSchema: "json"`. `PreToolUse`, `UserPromptSubmit`, and `PermissionRequest` can block; the rest are fire-and-forget. `SessionEnd` fires on every exit path (including Ctrl+C and external `SIGTERM`/`SIGHUP`), so it doesn't always have a real session id to report. All hooks for an event run in parallel with no defined ordering, and every configured hook always runs — there's no way to disable one without editing the file it came from.