local-code-worker
A local MCP server that lets a cloud coding agent (Claude Code, Codex, Cursor)
hand bounded, mechanical code generation to a model running on your own
machine, then review the result as a plain diff before anything touches the
repository.
The cloud model keeps the thinking: it decides what to change, picks the
exact files, reviews the diff and validates the result. The local model does
the typing. Output tokens are the slow and expensive part of generation, so
moving predictable code out of the cloud saves money and time without giving
up control.
cloud agent ──request_changes──▶ local-code-worker ──▶ LM Studio (local model)
▲ │
└──────── unified diff ◀───────────┘ repository untouched
cloud agent ──apply / revise / decline──▶ local-code-worker ──▶ repository
Why
Cloud agents are great at deciding what code should exist, and poor value
for money when they spend thousands of tokens typing boilerplate, mappings,
switch cases or a small change inside a large file. This worker hands that
typing to a local model and returns a deterministic diff the cloud agent can
review in a few hundred tokens.
The local model never gets filesystem, shell, search or git access. It only
sees numbered in-memory text buffers and answers with line edits:
edit_lines(path, start_line, delete_count, content)
The worker applies those edits to a virtual copy, builds the candidate, diffs
it against the original snapshot and keeps it in memory. The real repository
changes only when the cloud agent calls apply_changes.
Results
Measured on a 40-task evaluation with qwen3.8-27b-mlx through LM Studio on
Apple Silicon. These numbers describe this worker, model and protocol on
mechanical coding tasks, not a universal benchmark.
| Metric |
Value |
| Overall pass |
33/40 (82.5%) |
| First-pass |
27/40 (67.5%) |
| Single-file edits |
92.6% |
| Multi-file edits |
50% |
| File creation / deletion |
100% |
| Median / p90 generation |
6.5 s / 16.5 s |
Line edits instead of full-file rewrites are what make it worth it: a small
change in a ~1,400-line file took ~1.1k completion tokens and ~96 s instead of
~12.5k tokens and ~865 s for regenerating the whole file.
Full numbers, failure modes and routing advice:
skills/local-code-worker/references/routing.md.
What to delegate
| Route |
Tasks |
| Delegate |
one-file mechanical edits, a small change in a large file, enum/status mappings, switch and table additions, adapters, DTOs, boilerplate, predictable tests, explicit create/delete, refactors with a fixed design |
| With caution |
2-3 tightly coupled files (implementation + test), moderate logic with an exact spec, concrete revisions |
| Keep in the cloud |
architecture, concurrency or security design, ambiguous debugging, broad refactors, project-wide discovery, changes too small to be worth a round trip |
The agent-facing version of this guidance is the skill in
skills/local-code-worker/SKILL.md. Point
your agent at it (or install it as a skill) so it knows when delegation pays
off.
Setup on a Mac (step by step)
Tested on Apple Silicon with LM Studio and an MLX model. A 27B model in 8-bit
needs roughly 30 GB of free unified memory; pick a smaller coding model if
your Mac has less.
1. Install Go and LM Studio
brew install go
brew install --cask lm-studio
Open LM Studio once so it finishes its first-run setup. It ships the lms
command line tool. If lms is not on your PATH yet:
~/.lmstudio/bin/lms bootstrap
2. Download and load a model
In LM Studio, search for the model and download it (Discover tab), or use the
CLI:
lms get qwen3.8-27b-mlx # download
lms ls # list downloaded models and their keys
lms load qwen3.8-27b-mlx # load it into memory
lms ps # confirm it is loaded
Then, in the model's settings in LM Studio, turn Reasoning Section
Parsing OFF. With it on, Qwen's structured output is returned in
reasoning_content, message.content stays empty, and the worker rejects the
response. Details: docs/lm-studio.md.
You do not need to enable Structured Output in the UI; the worker sends a
JSON Schema response_format with every call.
3. Start the LM Studio API server
lms server start # serves http://127.0.0.1:1234
(Or Developer tab -> Start Server. Keep "Serve on Local Network" off.)
Check that the API answers and note the exact model id:
curl -s http://127.0.0.1:1234/v1/models
The id shown there is what you pass as -model below.
4. Build and run the worker
git clone https://github.com/valanced/local-code-worker.git
cd local-code-worker
go build -o bin/local-code-worker ./cmd/local-code-worker
./bin/local-code-worker -model qwen3.8-27b-mlx
Or install the binary straight into $(go env GOPATH)/bin:
go install github.com/valanced/local-code-worker/cmd/local-code-worker@latest
local-code-worker -model qwen3.8-27b-mlx
The daemon listens on http://127.0.0.1:7337/mcp. It starts even when LM
Studio is down; the model is only needed when a generation runs. Check it:
curl -s http://127.0.0.1:7337/healthz # -> ok
5. Keep it running in the background (optional)
The install script builds the binary into ~/.local/bin, writes a user
LaunchAgent, starts it and waits for the health check. It also restarts the
service after a rebuild, so rerun it after git pull.
scripts/install-launchd.sh
# different model or LM Studio URL:
LCW_MODEL=qwen/qwen3-coder-next scripts/install-launchd.sh
LCW_LM_STUDIO_URL=http://127.0.0.1:1234/v1 scripts/install-launchd.sh
scripts/status-launchd.sh
scripts/uninstall-launchd.sh
Logs: ~/Library/Logs/local-code-worker/. More:
docs/macos-service.md.
6. Connect your agent
Claude Code:
claude mcp add --transport http --scope user local-code-worker http://127.0.0.1:7337/mcp
Codex (~/.codex/config.toml):
[mcp_servers.local-code-worker]
url = "http://127.0.0.1:7337/mcp"
Cursor (~/.cursor/mcp.json):
{ "mcpServers": { "local-code-worker": { "url": "http://127.0.0.1:7337/mcp" } } }
All clients share one daemon and one in-memory request store. Finally, give
your agent skills/local-code-worker/SKILL.md
(install it as a skill or reference it from your agent instructions) so it
knows when to delegate.
7. Try it
Ask your agent for a small mechanical change, for example "add a String()
method for this enum, delegate the code to local-code-worker". You should see
request_changes return a diff, then apply_changes and record_outcome.
Troubleshooting
| Symptom |
Fix |
model is required / wrong model picked |
Pass -model with the exact id from /v1/models |
| Response has reasoning but no content |
Turn Reasoning Section Parsing OFF for the model |
| Output truncated |
Raise -max-tokens, or split the task |
| Timeouts on large edits |
Raise -timeout (default 5m) |
health check failed after install |
tail ~/Library/Logs/local-code-worker/stderr.log |
| Tool |
What it does |
request_changes |
Capture explicit read_files / write_files from repo_root, ask the local model for a candidate, return a unified diff. Creates a pending request. Repository untouched. |
revise_changes |
Send concrete feedback on a pending candidate and get a new diff against the original snapshot. Limited by -max-revision-attempts. |
apply_changes |
Re-verify every captured source by hash, then write the candidate. If anything changed on disk since capture, returns stale and writes nothing. |
decline_changes |
Drop a pending candidate without touching the repository. |
record_outcome |
After apply and validation, record accepted (files still match the candidate) or cloud_fixed (the cloud agent had to repair it), with line-level repair stats. |
A typical session:
- The cloud agent investigates and decides the exact change and files.
request_changes with a precise brief and explicit file lists.
- The agent reads the diff. Wrong in a concrete way:
revise_changes with a
specific correction. Fine: apply_changes. Hopeless: decline_changes.
- The agent runs the project's formatter and tests.
record_outcome, then stop.
The full contract, including every status and edge case, is in
docs/mcp-contract.md.
Configuration
| Flag |
Default |
Meaning |
-listen |
127.0.0.1:7337 |
Listen address; loopback only |
-mcp-path |
/mcp |
MCP Streamable HTTP path |
-lm-studio-base-url |
http://127.0.0.1:1234/v1 |
LM Studio API; loopback only |
-model |
auto |
Model id; required when LM Studio exposes more than one |
-timeout |
5m |
HTTP deadline for one generation |
-max-tokens |
16384 |
Completion cap per generation |
-max-revision-attempts |
3 |
Model calls allowed per request for revisions |
-max-file-bytes / -max-total-bytes |
0 (unlimited) |
Capture size limits |
-request-ttl |
24h |
Evict requests idle longer than this; 0 disables |
-temperature, -top-p, -top-k, -presence-penalty, -repeat-penalty |
Qwen defaults |
Sampling |
Safety model
- Loopback only: the daemon refuses non-loopback listen addresses, the MCP SDK
rejects non-localhost
Host headers (DNS rebinding), and cross-origin browser
requests are blocked.
- No authentication: any local process can call it, and it can write inside
any
repo_root it is given. Run it only on a machine you trust.
- Explicit scope: only listed files are read or written. Absolute paths,
.. traversal and symlinks (file or any parent directory) are rejected.
- Text only: non-UTF-8 files are rejected.
- Nothing is written before
apply_changes, and apply re-checks every source
hash first. Writes are atomic per file (temp file + rename). A failure after
verification can still leave earlier files of the same candidate written;
V1 does not roll back.
- Requests live in memory and are lost when the daemon restarts. File
contents are kept only as long as a request needs them: sources until
apply or decline, the applied candidate until
record_outcome. Requests
idle longer than -request-ttl (24h) are evicted.
Development
go build ./...
go vet ./...
go test ./...
cmd/lcw-*-smoke are manual smoke tools against a live LM Studio.
Design notes: docs/architecture.md,
docs/model-protocol.md,
docs/model-prompt.md.
Status
V1, used daily by the author. Tested with one model on macOS. Other
OpenAI-compatible local servers may work if they support JSON Schema
response_format, but only LM Studio is tested.
License
MIT, see LICENSE.