I burned 20% of a weekly Grok Bot allowance in thirty minutes. Seven or eight bots running in parallel, all doing real work, all metering hard. The product is genuinely good; that was the frustrating part. The bottleneck wasn't capability. It was inference budget.
I already pay for Claude and for ChatGPT. Those subscriptions have their own limits, and they were sitting completely idle while Grok Bot ground through its quota.
So I built Locum, a provider adapter that lets Grok Bot delegate coding work to the agent CLIs already authenticated on my own machine. A locum is a qualified professional who temporarily does someone else's job, which is exactly the arrangement.
This post is about how it works, and more importantly about the one architectural decision that separates it from the version of this idea that gets you banned.
The constraint that decides everything
Grok Bot doesn't run on your laptop. Each bot gets a persistent computer in xAI's cloud, which is what makes it useful: it keeps working after you close the lid. It also means the bot cannot see localhost. There is no local plugin surface to hook into.
What it does have is documented: custom MCP connectors. You point it at a public HTTPS endpoint speaking the Model Context Protocol, it discovers your tools, and it can call them. xAI even publishes a guide for tunnelling a server running on your own machine.
That single fact fixes the shape of the whole thing. Locum has to be a remote MCP server that reaches back to your machine, not a patched binary and not an intercepted protocol.
The line I would not cross
There are two ways to build "use your Claude subscription somewhere else," and they are not close to each other.
The tempting one: read the OAuth token that Claude Code stores, and call Anthropic's API yourself with it. It works. It is also explicitly prohibited. Anthropic's own Claude Code legal page says OAuth is "intended exclusively" for ordinary use of Claude Code and other native Anthropic applications, and that they do not permit third-party developers to route requests through Free, Pro, or Max plan credentials on behalf of their users. People have shipped tools on the other side of that line and watched them get shut off.
The other way: don't handle credentials at all. Spawn the official binary.
The credential boundary. Locum is a launcher, not a proxy.
Locum runs claude -p "<prompt>" as a subprocess. That's it. The CLI you already logged into does the authenticating, exactly as it does when you type the command yourself. Locum never opens ~/.claude, never touches the keychain, never sees a token, and never makes a request to api.anthropic.com.
Four invariants are written at the top of the source file, and they're the reason the project is publishable:
- Never read credential files, keychains, or OAuth tokens.
- Never call
api.anthropic.comorapi.openai.comdirectly. - Only ever spawn the official
claude/codexbinaries. - Single-operator. One token, one allowlist of workspace roots.
One more thing it deliberately does not do: bypass Grok Bot's own entitlement. You still need Grok Bot access for a connector to exist at all. Locum changes where the inference comes from. It does not get you Grok Bot for free, and I'd have no interest in shipping the version that did.
The shape of it
Six tools cross that boundary: delegate_to_claude, resume_claude, delegate_to_codex, check_job, list_jobs, cancel_job.
About a thousand lines of Python across two files, no framework beyond an MCP server library.
Async is the entire trick
This is the part that decides whether the integration works at all, and it's easy to get wrong.
MCP tool calls are request/response with a timeout measured in tens of seconds. Real coding tasks take minutes. If delegate_to_claude tried to run the task and return the answer, every useful call would time out, and worse, it would time out after burning the work, with the result stranded in a dead subprocess.
Build this in from the start. Retrofitting it means rewriting the server.
So delegate_to_claude starts the job and returns a handle immediately. The bot polls check_job until the status changes.
The polling is more useful than it sounds. Locum runs the CLI with --output-format stream-json and parses the event stream as it arrives, so a poll returns not just "still running" but the turn count and the last few tool calls:
{
"job_id": "dacb5836aa66",
"status": "running",
"elapsed_seconds": 74.2,
"turns": 9,
"recent_activity": ["Read src/auth.ts", "Edit src/auth.ts", "Bash npm test"]
}
The bot can narrate real progress instead of staring at a black box. When the job finishes, the result comes back along with a session_id, which matters, because resume_claude reuses it. A cold delegation re-pays about 18k tokens of CLAUDE.md and system prompt setup; resuming hits the prompt cache instead. Follow-ups should always resume.
Grok speaks OAuth, and only OAuth
I built the first version with a bearer token. Simple, adequate for a single-operator tool, easy to test with curl.
Then I opened Grok's custom connector dialog and found no field for a header. It had probed my server, got a bare 401, and fallen back to asking me for OAuth app credentials: client ID, authorization endpoint, token endpoint, PKCE method.
That's the MCP specification's authorization flow, and it isn't optional. A remote MCP server that wants a real client has to be an OAuth 2.1 authorization server.
So Locum became one. It's less code than it sounds:
Discovery, dynamic registration, PKCE, refresh. No third-party OAuth app to create.
The consent screen is the load-bearing part, and it's worth being explicit about why. /authorize sits on a public tunnel. Without a gate, anyone who learned the URL could walk through the flow, mint themselves a token, and get shell access to my machine. So the consent page demands a passphrase and shows the redirect target before you approve. If that destination isn't the one you expect, you're looking at someone else's authorization attempt.
Codes are single-use and expire in 120 seconds. S256 is required; plain is refused. Every comparison is constant-time. There's a test file that exercises all of it: discovery, registration, the consent gate, PKCE enforcement, replay rejection, refresh. Security code that isn't tested is decoration.
Three bugs worth your time
The architecture took an afternoon. These took longer.
The 404 that was a port collision
Everything started returning 404. Every path, including /health, which is an unconditional route. The tunnel reported healthy, ingress validated, the routing rule matched.
The response was content-length: 10. Exactly the length of Not found., and that string wasn't in my server.
The Grok Bot app itself listens on port 8787. macOS prefers ::1, so the tunnel talked to it instead.
The Grok Bot desktop app listens on [::1]:8787. Locum was on 127.0.0.1:8787. Same port, different address families, so there was no "address already in use" error, no warning, nothing. macOS resolves localhost to ::1 before 127.0.0.1, so every tunnelled request reached the wrong process.
Two things found it. lsof -nP -iTCP:8787 -sTCP:LISTEN showed both listeners, one line each. And cloudflared --loglevel debug logs ingressRule= and originService= per request, which separates "cloudflared's catch-all matched" from "the origin returned this". I'd assumed the former and was wrong.
Two fixes, because either alone leaves a trap: move to port 8791, and target 127.0.0.1 explicitly rather than localhost.
The service daemon that installed itself broken
sudo cloudflared service install writes a launch daemon whose ProgramArguments is just the binary path. No tunnel run. No --config. It runs as root, whose $HOME is /var/root, so ~/.cloudflared/config.yml is invisible to it, and it crash-loops on a 5-second KeepAlive.
The failure hides itself perfectly: the tunnel keeps working, because your user-level cloudflared tunnel run is still serving. You end up with a crash-looping root daemon and a working user process registered as two connectors for one tunnel. Adding a connector while that's in flux is how you get "Connection failed" from a server that answers every external probe.
The fix is to put the config where root can read it, /etc/cloudflared, and rewrite credentials-file to match.
The nested-session auth failure
Every delegated job died with Failed to authenticate: OAuth session expired and could not be refreshed.
Claude Code exports session plumbing into every child process it spawns: CLAUDECODE, a family of CLAUDE_CODE_* variables, and ANTHROPIC_BASE_URL. A claude that inherits those believes it's a nested child session and tries to delegate authentication to a host socket that isn't listening.
Locum now strips them before spawning, but only when it detects it's nested, so a deliberately-set ANTHROPIC_BASE_URL still works in a normal terminal.
The lesson generalises past this project: when you spawn a CLI from a server, you inherit an environment you did not design. Decide what crosses that boundary.
Four things I only learned by using it
The version above worked. Using it for a day surfaced the rest.
An approval prompt is not a safety feature, it is a hang
Jobs involving gh, or an install, or anything else the agent wanted to confirm
would simply stop. Not fail: stop. The client sat waiting on a job that would
never finish, because the approval prompt was on my Mac and the thing that asked
for it was a bot in someone else's cloud.
A prompt nobody can answer does not protect you. It just converts a task into a
timeout. Delegated jobs now run with --dangerously-skip-permissions and
--dangerously-bypass-approvals-and-sandbox, and the honest consequence is that
the token and a narrow workspace allowlist are what protect you, not the agent's
permission model. The allowlist bounds where a job starts, not where a shell
command it runs can reach. That trade is written down in SECURITY.md rather
than glossed, because the previous version of that file said the opposite.
The model and the effort should be a decision, not a default
Some delegations want the cheapest model that can follow instructions. Some want
the deepest reasoning available, once. So every delegate tool takes model and
effort, under one vocabulary across both CLIs: Claude spells it --effort,
Codex spells it -c model_reasoning_effort and requires that -c precede the
subcommand. A caller should not have to know either fact.
The interesting part is resume_claude taking them too. Escalating a resumed
session keeps the context you already paid for, so "start cheap, escalate on the
turn that actually needs it" costs far less than restarting at a higher setting.
Correct answers hide broken telemetry
Codex delegations returned the right results, so it looked fine. It was not.
The progress parser was reading an event stream Codex no longer emits, so turn
counts sat at zero and the activity feed showed the literal string
item.completed instead of what the agent was doing.
Worse, Codex can fail a turn and still exit 0. Those jobs were reported as
done with whatever happened to be left in the output file. Silently wrong
results are worse than visible failures, and only testing the thing I had not
launched with caught it.
You cannot demo a claim you cannot see
The MCP client shows a chat bubble. That is all. There was no way to show that work ran on my machine, what the agent did, or what it cost, short of grepping a log and matching line numbers against the real file.
So the server narrates every job on stdout, and there is a dashboard: every session with model, effort, turns, duration, tokens and cost, and a full transcript per job of each tool call with its arguments, the reasoning, and the output. Running jobs stream in live.
All three read one recorded stream, so they cannot drift apart.
Then the dashboard hung at "connecting" behind Cloudflare while working
perfectly on localhost. The stream sent its response headers and waited up to
twenty seconds for the first event; a proxy holds headers until some body
arrives, so the browser never saw a response at all and EventSource.onopen
never fired. Flushing a single comment immediately fixed it. Time to first byte
through the tunnel went from never to 0.4 seconds.
That is the third bug in this project that only existed behind the proxy, after the port collision and the OAuth callback. Anything streaming or long-lived has to be tested through the tunnel, because a proxy changes the semantics, not just the latency.
The tool found its own bugs
Once the dashboard existed, the obvious thing to try was pointing Locum at its
own newest code. I delegated a review of the Codex event parser, written about
an hour earlier, at model: opus, effort: high.
It came back with five findings. All five were real.
The dangerous one: the parser called .get() on whatever json.loads returned.
A bare string, a number, a list are all valid JSON, so a line like "error"
raised AttributeError, which killed the stdout reader. The runner then skipped
both proc.wait() and its cleanup step, leaving the Codex child unreaped and
its temp file on disk. On a server designed to run for weeks under launchd.
I reproduced all five before fixing them. The subtlest was that a mid-stream
error flipped a job out of running immediately, which broke cancellation, let
the history pruner evict a job that was still executing, and masked a later
success.
Later, during the demo recording, a delegated job added a --version flag to
the server and ran the test suite to confirm nothing broke. Its summary of what
it changed matched the diff. It did put the check at the top of __main__,
which is after the module-level token guard has already exited, so
server.py --version refused to answer without a secret. That one I fixed by
hand.
Both of those are the honest version of what this tool is for: it is very good, it is not unsupervised, and the difference matters.
What it actually saves
Honesty matters more than the pitch here, so:
It reduces Grok Bot usage. It does not eliminate it. The bot's own orchestration turns still meter. The win is collapsing fifty bot steps into one tool call and a few polls, moving the long agentic grind onto the subscription you already pay for.
Your machine has to be awake with the tunnel up. It's a personal tool, not a service.
Those subscriptions have limits too. This changes where the inference budget comes from. Nobody found an infinite-token glitch.
The skill file matters more than the tools. Without an instruction telling the bot to prefer delegating, it has the tools available and keeps grinding through its own loop anyway. That one markdown file is the difference between the integration working and merely existing.
Running it
It's open source. You need claude or codex already signed in, a domain on Cloudflare for a stable hostname, and Grok Bot access.
git clone https://github.com/HarjjotSinghh/locum
cd locum && cp .env.example .env # set a token and your workspace roots
set -a && source .env && set +a
uv run server.py
Then a tunnel, a custom connector pointed at https://your-host/mcp, and the skill file pasted into a Grok Bot Skill.
The invariants at the top of server.py aren't decoration. If a change would break one of them, it's the wrong change, and that is what keeps this a tool worth using rather than a liability worth avoiding.
Not affiliated with or endorsed by xAI, Anysphere, OpenAI, or Anthropic.