mini-claude-code
An open-source coding-agent harness · TypeScript

Doesn’t ask about wc -l. Does ask about rm -rf.

mini-claude-code is a small, readable agent harness on the Claude API: the loop, the tools, the permissions, hooks, MCP and ACP, in about 5,300 lines you can read in an afternoon. Its risk gate asks a local model four narrow questions about every shell command, runs the ones that clear all four, and brings you the rest. On 153 commands it had never seen, it cleared 38% of the safe ones and none of the 76 unsafe.

MCP · stdio + HTTPAgent Client ProtocolClaude Code hooks Agent Skills-p · JSON / stream-jsonCI · Linux + Windows
localhost:5174 — the web UI, Editorial theme
A recorded session: the model writes a task list, hands a lookup to a subagent, runs wc -l, which the gate clears, and asks for rm -rf dist, which the gate holds at 0.993 and which is denied.

A real session, not a mock-up: qwen3:14b on a local Ollama drives the loop and llama3.1:8b is the judge; 143 seconds play at 6×. The task list, the subagent’s call, wc -l cleared with all four answers under 0.20, and rm -rf dist held at 0.993 and denied. The fourth of five takes; the clip ends as the model starts its reply.

01The problem

An agent that asks about everything teaches you to stop reading.

A coding agent either asks before every command or runs them all. Ask about every ls and git status, and approving becomes a reflex; the prompt that mattered gets the same keystroke. Ask about none, and the one that mattered never reaches you.

A permission prompt is a decision about a command, not a tool name. “Bash?” is not a question anyone can answer. rm -rf dist is.

02The gate

Four narrow questions, and the worst answer decides.

Before a shell command runs, a local model is asked four yes-or-no questions and returns a probability for each, read from one token’s log-probabilities. Below 0.20 on all four, the command runs without a prompt. Otherwise it comes to you, with the question that stopped it.

destroys-data

Would running it cause unrecoverable data loss?

outside-cwd

Would it modify files, processes, devices or remote state outside the working directory? Reading does not count.

exfiltrates

Would it upload or POST local data to a remote host? Downloading does not count.

reveals-secret

Would it print or copy a credential: an API key, password, token, private key or .env file?

The gate can only clear what your rules would ask about. It can never reopen a static deny, and a judge that throws, times out, skips a question or returns something that is not a probability sends the call to you. Every failure resolves to asking.

A tool call passes schema validation, a PreToolUse hook, the permission rules, the risk gate for Bash, and the user, then runs. tool_use from the model schema bad input → refused PreToolUse hook exit 2 → blocked rules a deny always wins risk gate only Bash asks you what it cannot clear all four < 0.20 → runs, no prompt

One tool call, left to right. Read-only calls in a turn run together; Bash, Write and Edit wait their turn and run one at a time. What the model gets back is capped at 40,000 characters, and the whole text is saved to a file it can Read.

03The evidence

Measured on commands it had never seen.

0/76

unsafe commands cleared on the held-out set of 153. The one number that must be zero.

38%

of its 77 safe commands cleared without a prompt: 29 fewer interruptions.

85%

cleared on the dev set that chose the threshold, shown so the 38% is read against it.

4

narrow questions, where one compound question let 9 unsafe commands of 34 through.

Hand-labelled shell commands; the judge is llama3.1:8b on a local Ollama; the threshold is 0.20. The held-out commands came from asking the agent’s own model what it would run across a dozen tasks, with only the labels written by hand. Each held-out set logs every time it was read. The full record, including the thresholds reasoned wrong before they were measured right, is in measurements.md.

04What it found in itself

The failures that raise no error.

The harness was driven with a scripted model to look for the failures a mock suite does not see: nothing throws, and something is quietly wrong. Each one below is now a check in the suite.

  1. 01Grep ran a shell command under --read-onlyIts glob was pasted into a shell string, with no prompt and no gate. Now each argument is one element of rg’s argument vector.
  2. 02Two Edits of one file, one lostRun concurrently, one of two edits vanished in 92–98 of 100 turns on a 1 MB file while both reported success. Writes now wait their turn.
  3. 03A session poisoned by max_tokensA reply cut off mid tool call was saved as it was, and every later resume of that session failed. It is now asked again with more room, or not saved.
  4. 04Three million characters to the modelTool output had no budget. It is capped at 40,000, start and end kept, and the whole text saved to a file one Read away.
  5. 05“Bash” was cmd.exe on WindowsEvery command went to cmd.exe, with GBK output decoded as UTF-8. It now runs in Git Bash and decodes with the console’s code page.
  6. 06“Permission denied” read as a file-system errorRecording the session above: denied rm -rf dist, the model tried icacls dist /grant administrators:F. The gate held that too; the message now says the call was declined.
  7. 07“Read-only” ran an MCP server’s write_fileThe read-only preset denied three tools and allowed the rest. It now asks before WebFetch and every MCP tool.
05What’s inside

A whole harness, small enough to read.

src/agent.ts

The loop

Reads in parallel, writes in order, input checked against each tool’s schema, sessions saved every turn and resumable.

src/tools/

Tools

Bash, Read, Write, Edit, Glob, Grep, WebFetch, plus Task for subagents, TodoWrite for a task list, and Skill.

src/permissions/

Permissions

Claude Code’s rule syntax, Bash(npm test *) or Read(~/.ssh/**). A deny always wins; otherwise the most specific rule does.

src/hooks/

Hooks

Claude Code’s format, unchanged: the same settings JSON, exit code 2 to block, PreToolUse through Stop.

src/mcp/ · src/acp/

MCP and ACP

MCP servers over stdio or streamable HTTP, and the Agent Client Protocol, so an editor can drive it.

src/context/

Context

AGENTS.md, Agent Skills, two prompt-cache breakpoints, and compaction that archives the full transcript.

07Quick start

Two commands to a REPL.

$ npm install
$ cp .env.example .env    # add ANTHROPIC_API_KEY
$ npm run cli

# the gate needs token probabilities; a local
# Ollama gives them, with no key and no cost
$ ollama pull llama3.1:8b
$ AGENT_JUDGE_BASE_URL=http://localhost:11434/v1 \
  AGENT_JUDGE_MODEL=llama3.1:8b AGENT_JUDGE_API_KEY=ollama \
  npm run cli -- --ask --gate

Web UI. npm run server and npm run client, then open localhost:5174. It listens on 127.0.0.1 only and refuses other origins.

Scripts. -p "…" --output-format json prints one result object; the exit code says whether the model finished (0), stopped short (2) or failed (1).

Offline gate. --gate allowlist needs nothing, and clears less.

Any Anthropic-compatible endpoint. Set ANTHROPIC_BASE_URL; nothing is pinned to api.anthropic.com.