local-agent-mcp
An MCP server that lets Claude Code hand work to a model on your own machine — and a sandbox tight enough that letting it is reasonable.
- Role
- Sole author — design, implementation, tests
- Year
- 2026
- Status
- Not yet released
- Stack
- Python · MCP · Ollama

The constraint
Cloud tokens cost money, and some code should not leave the machine. A model running locally solves both — but handing an autonomous loop a filesystem and a shell is how you lose an afternoon, or a directory.
So the design question was never the plumbing. It was blast radius. Three decisions follow from that, and each one cost something.
Resolve the path, don’t compare the string
Every path goes through resolve_within, which realpaths both the base and
the candidate before comparing them with commonpath. Symlinks and .. are
resolved before the check rather than after, and an absolute path escapes the
join rather than defeating the guard.
What it cost: slower than a prefix comparison, and every call can now fail
for a reason the caller has to handle. In exchange the obvious bypasses don’t
work — ../../../etc/passwd, an absolute /etc/passwd, and sub/../../out.txt
are all refused, and there are tests saying so.
A turn cap, not a stop condition
The agent loop is hard-capped at three turns. Control returns to the caller whether or not the local model believes it has finished.
What it cost: a task that genuinely needs a fourth turn fails, and the caller has to split it. Worth it — a model that can’t tell it’s stuck can’t be trusted to decide when to stop.
The allowlist is a guardrail, and it says so
Commands are parsed rather than handed to a shell, and matched on the program rather than the raw string. The documentation now states plainly what that protects against: a local model making a mistake, not an adversarial one.
What it cost: no pipes, no redirects. And it cost me the embarrassment of
finding the first version wrong. That version glob-matched the whole command
string under shell=True, so a pattern as tight as git * still let
git status; echo PWNED through — both halves ran. The README claimed the
mitigation was “scope patterns tightly”, which was advice that could not work.
What I’d do differently
I wrote the safety documentation before I tested it, and it was wrong for
months. The lesson isn’t “write tests” — there were tests, including a
test_safety.py. It’s that a claim in a README deserves a test of its own.
The tests covered what the code did. Nothing checked whether the code did what
the documentation promised.