Turning Claude Code into a pure LLM endpoint
SmartBrain is a self-hosted assistant that runs on local models by default. We wanted an
optional extra: use the Claude subscription many people already pay for as a plain model
backend — through the claude CLI, so there is no API key and no per-token bill.
That means driving a full coding agent as if it were a bare model. Locking it down
took real work, and the defaults surprised us. Here is what leaked, how we found it, and the
exact recipe that seals it — all of it enforced by tests in our suite, not just promised.
Surprise #1: the obvious tool-blocking flag blocks nothing
Claude Code can read files, run shell commands, and browse. For a chat backend all of that
must be off — the assistant embedding it has its own tools behind its own approval gates.
The flag that looks right is --disallowedTools "*". We tested it by asking the
model to run echo hi with its Bash tool:
{"type":"tool_use","name":"Bash","input":{"command":"echo hi"}}
{"type":"tool_result","content":"hi","is_error":false}
It ran. The wildcard is not a tool name, so it matches nothing, and there is no error telling
you so. What actually works is defining a custom agent whose tool set is empty and
selecting it for the session. The init event then reports "tools": [], and the
model itself confirms it has no Bash access when asked to use it.
Surprise #2: your global CLAUDE.md rides into every session
We asked the contained session what personal context it could see. The answer, verbatim from
the default configuration: the user's private ~/.claude/CLAUDE.md, quoted back
with its path. A chat backend must not see the operator's personal instructions file. The fix
is --setting-sources "" — load no user or project settings at all. We A/B-tested
the same question with and without the flag: with it, the model reports no file access and no
CLAUDE.md. What still reaches the model afterwards: the current date, and the email address of
the signed-in Claude account — the account the chats already go to.
Surprise #3: argv is public
Every flag you pass is visible to any process on the machine via ps. So nothing
conversation-derived can go on the command line: our agent definition on argv is a static
constant, and the system prompt, tool descriptions, and transcript all travel over stdin. Our
test asserts the argv is byte-identical across turns.
The recipe
claude -p --verbose \
--output-format stream-json --include-partial-messages \
--no-session-persistence \
--setting-sources "" \
--strict-mcp-config \
--model sonnet \
--agents '{"backend":{"description":"chat backend",
"prompt":"<static pointer to stdin>","tools":[]}}' \
--agent backend
"tools": []— no file, shell, or web access (the part--disallowedToolswon't give you).--no-session-persistence— no transcript on disk.--setting-sources ""— no user/project settings or CLAUDE.md in the session.--strict-mcp-config— MCP servers from ambient config are structurally ignored.stream-jsonrequires--verbosein print mode — the error tells you, eventually.
Three environment details round it out:
- Set
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1— the CLI's documented opt-out of telemetry and error reporting, so the conversation is the traffic the feature adds. - Strip
ANTHROPIC_API_KEYfrom the child's environment — an inherited key silently switches the CLI from the subscription to API billing. - Run from a private
0700working directory, never a shared/tmp— on multi-user Linux, anyone can pre-place a.claude/agents/or.mcp.jsonin a shared cwd for your session to trip on.
Process hygiene, learned the hard way
An adversarial review of our first implementation found two ways it could wedge: stderr piped
but never drained (the CLI is chatty under --verbose; a full pipe stalls it
mid-answer), and a deadline that was only checked when output arrived — a hung CLI would block
the reader forever. The fixes: merge stderr into stdout and filter, feed stdin from a helper
thread, enforce the deadline with a watchdog timer that kills the child's whole process group
(start_new_session + killpg — a helper process holding the pipe open
survives a plain kill), and always reap.
The payoff
The CLI becomes a well-behaved model backend: streaming tokens, tool use via a plain-text protocol our agent loop already speaks, token counts and API-equivalent cost reported per call (the CLI prices itself — no rate table to go stale), and the user's plan-window status straight from its rate-limit events. Subscription in, model out.
The full implementation — containment tests included — is open source: SmartBrain_3000, with the user-facing docs in Connect a model. Verified against Claude Code 2.1.x; flags move fast in this ecosystem — test, don't trust, including us.