local-vibes
Delegate coding grunt-work to a local model. A generic OpenAI-compatiblesubagent MCP server for Claude Code (and any MCP host).
It exposes one tool, delegate, that runs a real tool-calling agent loop(read → edit → run → iterate) against any OpenAI-compatible chat-completionsendpoint — Ollama, LM Studio, vLLM, or a remote API. The host's own model andauth are never touched: this is a plain MCP tool call, so a Max-subscriptionClaude Code primary can offload grunt work to a local model with noANTHROPIC_BASE_URL change.
Backend, model, limits, and concurrency are all environment variables — nothingis model-specific. Point it at a different endpoint and it just works.
How it works
Claude Code (primary, on your subscription)
└─ tool call: delegate(task, working_dir, read_only) ── may fan out many in parallel
└─ bounded-concurrency gate (LOCAL_VIBES_MAX_CONCURRENCY)
└─ agent loop on your local model:
read_file · list_dir · grep · write_file · edit_file · run_bash · finish
(file/shell ops sandboxed to working_dir; bounded by max iters)
└─ returns: summary + git change report of what the subagent did
Install as a Claude Code plugin (recommended, self-installing)
This repo is a self-installing Claude Code plugin. Point Claude at it and say"install local-vibes", or run the script yourself. It registers three removablethings — the MCP delegate tool, a skill that tells Claude when to use it, and atargeted hook — and never edits any CLAUDE.md:
git clone https://github.com/OpenSourceWTF/local-vibes.git ~/projects/local-vibes
~/projects/local-vibes/scripts/install.sh # generic default backend (Ollama)
Point it at any OpenAI-compatible server (it is not tied to any one backend):
LOCAL_VIBES_BASE_URL=http://127.0.0.1:8080/v1 \
LOCAL_VIBES_MODEL=my-local-model \
~/projects/local-vibes/scripts/install.sh
The delegate tool and the skill load in your next claude session. Toremove everything (MCP server + plugin + marketplace), in one command:
~/projects/local-vibes/scripts/uninstall.sh
Requires Node ≥ 18 and the claude CLI on PATH.
Install (manual MCP, no plugin)
Requires Node ≥ 18 and an OpenAI-compatible endpoint serving atool-calling capable model (e.g. Ollama: ollama serve + ollama pull qwen3-coder:30b).
Once published to npm, add it to Claude Code with npx (no global install):
claude mcp add local-vibes \
-e LOCAL_VIBES_BASE_URL=http://localhost:11434/v1 \
-e LOCAL_VIBES_MODEL=qwen3-coder:30b \
-e LOCAL_VIBES_MAX_CONCURRENCY=4 \
-- npx -y local-vibes
Before publishing (local dev), build once and point at the entry directly:
npm install && npm run build
claude mcp add local-vibes \
-e LOCAL_VIBES_MODEL=qwen3-coder:30b \
-- node /ABSOLUTE/PATH/TO/local-vibes/dist/index.js
To publish: npm publish (the prepublishOnly script builds dist/ first).
Make the primary use it
Add to your project's CLAUDE.md:
For codebase searches, boilerplate, mechanical multi-file edits, and testscaffolding, use the
delegatetool (local subagent) with a completeself-contained task and theworking_dir, instead of doing it inline.
Configuration (environment variables)
| Variable | Default | Meaning |
|---|---|---|
LOCAL_VIBES_BASE_URL |
http://localhost:11434/v1 |
OpenAI-compatible endpoint |
LOCAL_VIBES_API_KEY |
not-needed |
Sent as the key; local servers ignore it |
LOCAL_VIBES_MODEL |
qwen3-coder:30b |
Model name on that endpoint |
LOCAL_VIBES_MAX_ITERS |
25 |
Max model↔tool round-trips per task |
LOCAL_VIBES_TEMPERATURE |
0.1 |
Default sampling temperature (per-call temperature overrides) |
LOCAL_VIBES_LANE |
(unset) | Default sticky-session id, sent as the standard OpenAI user field (per-call lane overrides) |
LOCAL_VIBES_LANE_HEADER |
(unset) | Optional: also send the lane under this custom header (opt-in; for header-routing servers) |
LOCAL_VIBES_MAX_CONCURRENCY |
8 |
Cohort width — concurrent delegate loops offered to the backend (see below) |
LOCAL_VIBES_EXTRA_BODY |
{} |
JSON object merged into every request body — superset passthrough (see below) |
LOCAL_VIBES_ALLOW_BASH |
1 |
0 disables the run_bash tool entirely |
LOCAL_VIBES_BASH_TIMEOUT |
120 |
Per-command timeout (seconds) |
LOCAL_VIBES_TOOL_RESULT_CAP |
12000 |
Max chars of any tool result fed back to the model |
Model selection
The model field is required by the OpenAI /v1/chat/completions protocol —every request must carry one, so local-vibes always sends LOCAL_VIBES_MODEL.But the value is just a routing key, not a client-side choice of weights:
- On a multi-model host (Ollama with several tags, a router), it selectswhich model answers — e.g.
qwen3-coder:30b. - On a pinned single-model server (
mlx_lm.server, llama.cpp server, a vLLMserving one model), set it to whatever stable name that server exposes andforget it. If the server publishes an alias likedefault_modeland swapsthe real weights behind it, pointLOCAL_VIBES_MODELat the alias once — theclient never needs to change when you swap the underlying model.
Example: a local MLX server with a stable alias + high concurrency
claude mcp add local-vibes \
-e LOCAL_VIBES_BASE_URL=http://127.0.0.1:8080/v1 \
-e LOCAL_VIBES_MODEL=default_model \
-e LOCAL_VIBES_MAX_CONCURRENCY=8 \
-- npx -y local-vibes
(Point at the OpenAI chat endpoint directly, not an Anthropic/Responsestranslation gateway — local-vibes speaks /v1/chat/completions.)
Concurrency & scaling
The host can fan out many delegate calls at once. local-vibes runs themthrough a bounded-concurrency gate so they don't overwhelm a single backend:up to LOCAL_VIBES_MAX_CONCURRENCY loops run simultaneously and the rest queue.
Tune it to your backend's parallel width. Local inference servers processrequests in batched / lockstep decode. How far throughput scales depends on theserver: a naive one degrades once you oversubscribe its slots, while a propercohorting driver (mtplx PR #200) keeps climbing far past 16. Set concurrency towhat your backend actually rewards:
- Ollama — set concurrency to
OLLAMA_NUM_PARALLEL(the number of parallelslots Ollama serves). Going higher just makes requests queue inside Ollama. - vLLM / TGI — these batch aggressively; set concurrency near the server'smax batch size to saturate the GPU without over-queuing.
- Batching MLX servers (
mlx_lm.server/ MTPLX) — aggregate throughput riseswith streams — and on a real cohorting server (mtplx, PR #200) it keeps risingwell past 16, not collapsing. mtplx's own clean sweep on Qwen3.6-35B-A3B (eachbatch at its own cohort, code prompts) reads: aggregate 374 → 479 → 563 → 612→ … → 813 tok/s at B = 8 → 16 → 24 → 32 → … → 256, with ms/token bottomingout around B≈96–128. So there is no distinct-prompt collapse with the PR #200batched driver — my earlier "peaks at 8" was a stale stock-mlx_lm.servernumber. On mtplx the two useful operating points are the lanes:- 8 = the spec lane (MTP draft+verify, 2 rows/request → the 16-row M1–M16kernel budget): keeps speculation and concurrency. The default.
- 16 = the AR lane (1 row/request, no MTP): higher aggregate throughput.Both run on the tuned M1–M16 Metal kernels (row-owned MoE router, NAX verify,combine tail, moepack, GDN, ragged attention); above 16 rows the forward dropsto stock MLX kernels (any width, slightly slower/token).
- Concurrency of
1serializes everything (safe but leaves the batch idle); thedefault8fills the spec/MTP cohort. Raise to16for the AR lane, or higheron a server whose curve keeps climbing (mtplx does, to ~256).
To actually use N server-side streams you need N concurrent delegate calls inflight: a single delegate loop is sequential (each step waits for the previoustool result), so the host must fan out that many delegations andLOCAL_VIBES_MAX_CONCURRENCY must be ≥ N.
Named lanes (gateway routing)
If a router/gateway maps model names to different backends — e.g. a LiteLLM proxyexposing qwen (a thinking model) and qwen-instruct (a fast non-thinking modelon a separate port) — point LOCAL_VIBES_BASE_URL at the gateway and setLOCAL_VIBES_MODEL to the lane you want; the model field is the lane selector.Route grunt delegations to the fast non-thinking lane. Note: lanes that share oneGPU also share that ~8-stream ceiling — naming a lane is for routing (task type,thinking vs not), not for multiplying total concurrency.
File safety under concurrency: the inner tools are synchronous, so Node'ssingle thread makes each file operation atomic — concurrent subagents can neverinterleave a read-modify-write on the same file. There is no filesystem datarace to guard against. What the gate does not prevent is two subagents makinglogically conflicting edits to the same files; for independent parallel tasks,give each its own working_dir (or a separate git worktree) and merge theresults.
Strict superset & mtplx concurrency kernels
local-vibes is a strict superset OpenAI client: by default it sends onlystandard /v1/chat/completions fields, so it works against any OpenAI-compatibleserver (Ollama, LM Studio, vLLM). But an mtplxserver accepts arbitrary extra request fields (extra="allow"), so you can carrymtplx-specific hints without breaking portability, via LOCAL_VIBES_EXTRA_BODY:
claude mcp add local-vibes \
-e LOCAL_VIBES_BASE_URL=http://127.0.0.1:8080/v1 \
-e LOCAL_VIBES_MODEL=mtplx-qwen36-27b-optimized-speed-v2 \
-e LOCAL_VIBES_MAX_CONCURRENCY=8 \
-e LOCAL_VIBES_EXTRA_BODY='{"top_k":20}' \
-- npx -y local-vibes
The object is merged into every request body verbatim; a generic server thatdoesn't understand a field never receives one (empty default) or ignores it.
How the concurrency kernel engages. mtplx's cross-request batched decode runsB concurrent streams as one forward, amortizing a single dense-weight read acrossthe cohort (≈×2.2 net-ragged at B=8) and committing byte-identical greedy tokensper stream. local-vibes feeds it the way it needs to be fed: it fires up toLOCAL_VIBES_MAX_CONCURRENCY concurrent, independently-sequential delegate loops,so a cohorting server can batch them. Set the cohort width to 8 (the mtplxTHROUGHPUT-preset width).
Two things gate whether the cohort actually forms, and both are server-side:the server must run a batching preset (--scheduler-mode cooperative --batching-preset throughput, not the default serial/latency), and its servingpath must route through the batched-decode kernel (as of mtplx 2.5.2 that kernel isPhase-1 and not yet wired into the OpenAI serving path — the paged KV cache raisesat batch>1). Until then, concurrent requests are admitted and queued serially,which is safe and correct — local-vibes is already shaped to cohort the moment theserver serves the batched path.
The delegate tool
delegate(task, working_dir?=".", read_only?=false, temperature?, lane?)
- task — one complete, self-contained instruction, ideally with acceptancecriteria ("…and run the test to confirm it exits 0").
- working_dir — sandbox root; every file/shell operation is confined here.
- read_only —
truedisables all writes/edits/bash (safe for search andanalysis delegation). - temperature — per-call sampling temperature (overrides
LOCAL_VIBES_TEMPERATURE). Use0for deterministic output and to staycompatible with the greedy batched-decode cohort. - lane — sticky-session id, sent as the standard OpenAI
userfield (theportable, in-spec way to carry session/affinity — every OpenAI-compatible serveraccepts it). Reuse the same id across related delegations so a cohort-awareserver pins them to one lane; omit it and nouseris sent. All requests withina single delegate loop already share the lane, so a session is sticky byconstruction. For servers that route on a header instead, setLOCAL_VIBES_LANE_HEADERto also send it as that header.
Safety model
- Path confinement. Every path is resolved and rejected if it escapes
working_dir(lexically —../, absolute, and mixed escapes are blocked). Thesubagent cannot read or write outside the sandbox. - Bash is gated, not jailed.
run_bashruns withcwd=working_dirand atimeout, and can be turned off withLOCAL_VIBES_ALLOW_BASH=0. It is nototherwise sandboxed — a determined command can still reach the network orfiles the process can access. Pointdelegateat a repo you'd let a CI jobtouch, or run withread_only=true/ bash disabled for untrusted tasks. - The change report is read-only (
git diff --stat+ untracked list); itnever mutates your git index.
Development
npm install
npm run build # tsc → dist/
npm test # builds, then runs the invariant + concurrency tests
# run the loop directly against your backend, no MCP client needed:
LOCAL_VIBES_MODEL=qwen3-coder:30b \
npm run selftest -- "your task here" /path/to/working_dir # add --read-only to forbid edits
Caveats
- Output quality depends on the local model. The loop is correct, but smalllocal models vary a lot at sustained multi-tool-call orchestration. Verify thesubagent's diffs — treat it as a fast junior, not a trusted senior. Acoder-tuned model tool-calls far more reliably than a general chat model.
- Requires tool-calling support. The endpoint/model must support OpenAIfunction calling via
/v1/chat/completions. Pure text-completion models won'twork.