{"slug": "show-hn-locum-grok-bot-delegates-tasks-to-claude-codex-cli-on-your-machine", "title": "Show HN: Locum – Grok Bot delegates tasks to Claude/Codex CLI on your machine", "summary": "A developer released Locum, an open-source server that lets xAI's Grok Bot delegate coding tasks to the Claude Code and Codex CLIs running on the user's own machine via a custom MCP connector and a Cloudflare tunnel, preserving the user's own subscription credentials. The project explicitly prohibits hosting for third parties and enforces invariants such as never reading credential files or calling vendor APIs directly, to comply with Anthropic's terms that disallow routing requests through Free, Pro, or Max plan credentials on behalf of others.", "body_md": "*A locum is a qualified professional who temporarily does someone else's job.*\n\nLets Grok Bot delegate coding work to the Claude Code and Codex CLIs you are already logged into on your own machine, instead of burning Grok Bot usage on its own agent loop.\n\nNot affiliated with or endorsed by xAI, Anysphere, OpenAI, or Anthropic.\n\nGrok Bot runs on a persistent computer in xAI's cloud, so it cannot see\n`localhost`\n\n. It reaches this server over a tunnel, as a **custom MCP connector** --\na documented Grok feature, not a workaround.\n\n```\nGrok Bot (xAI cloud)\n   └─ MCP tool call ──► tunnel ──► this server (your Mac)\n                                      ├─ spawns `claude -p`   (your Claude sub)\n                                      └─ spawns `codex exec`  (your ChatGPT sub)\n```\n\nAnthropic's [Claude Code legal page](https://code.claude.com/docs/en/legal-and-compliance)\ndraws the boundary explicitly:\n\nAdvertised usage limits for Pro and Max plans assume\n\nordinary, individual usage of Claude Code and the Agent SDK.\n\nAnthropic does not permit third-party developers to offer Claude.ai login or to\n\nroute requests through Free, Pro, or Max plan credentials on behalf of their users.\n\nSo:\n\n| ✅ | You, your machine, your subscription, your own work |\n| ❌ | Hosting this so other people's requests hit your subscription |\nSharing the code so others run it on their own subscription -- fine only while the invariants below hold |\n\n**Invariants. Do not remove them; they are the reason this is legal.**\n\n- Never read credential files, keychains, or OAuth tokens.\n- Never call\n`api.anthropic.com`\n\n/`api.openai.com`\n\ndirectly. - Only ever spawn the official\n`claude`\n\n/`codex`\n\nbinaries, authenticated by the operator through the vendors' own login flows. - Single-operator: one bearer token, one allowlist of workspace roots.\n\nIf a change would break one of these, it is the wrong change.\n\nRequires `claude`\n\nand `codex`\n\nalready signed in, plus [uv](https://docs.astral.sh/uv/).\n\n```\ncp .env.example .env          # then set a real token and your roots\nset -a && source .env && set +a\nuv run server.py\n```\n\nTunnel it (Cloudflare quick tunnels do **not** carry SSE; this server uses\nStreamable HTTP, so they work fine):\n\n```\nbrew install cloudflared\ncloudflared tunnel --url http://127.0.0.1:8791\n```\n\nThat quick tunnel is fine for a first run, but it hands out a new hostname on every restart and Grok stores the URL -- so you would re-register and re-consent every time. For anything ongoing, take a stable hostname instead (needs a domain already on your Cloudflare account):\n\n```\ncloudflared tunnel login          # browser, once\n./setup-tunnel.sh locum.example.com\ncloudflared tunnel run locum\n```\n\n`setup-tunnel.sh`\n\nis idempotent: it creates the named tunnel if missing, points\nDNS at it, and writes `~/.cloudflared/config.yml`\n\n.\n\nTo keep the tunnel up across reboots, use the script, not `cloudflared service install`\n\n:\n\n```\nsudo ./install-service.sh\n```\n\n`cloudflared service install`\n\nwrites a launchd plist containing only the binary\npath, with no `tunnel run`\n\nsubcommand, so the daemon crash-loops while your\nuser-level tunnel quietly masks the failure. `install-service.sh`\n\nwrites the\nplist itself and verifies `/health`\n\nbefore claiming success.\n\nThat keeps the tunnel up. To keep the **server** up as well, so a reboot does not\nleave a healthy hostname pointing at nothing:\n\n```\n./install-agent.sh        # no sudo: it must run as you\n```\n\nTwo macOS requirements, both of which fail confusingly if missed:\n\n- If this checkout is under\n`~/Documents`\n\n,`~/Desktop`\n\n, or`~/Downloads`\n\n, grant**Full Disk Access to**. launchd agents do not inherit your terminal's TCC grants, and TCC judges the executable launchd starts.`uv`\n\n- Run\n`claude setup-token`\n\nand put the result in`.env`\n\nas`CLAUDE_CODE_OAUTH_TOKEN`\n\n. A launchd agent does not get your login session's credential access, so delegation fails with \"OAuth session expired\" even though the server itself starts fine.\n\nRegister at `grok.com/connectors`\n\n-> **New Connector** -> **Custom**, with the\ntunnel URL plus `/mcp`\n\n.\n\nGrok's custom connectors speak **OAuth 2.1 only** -- the dialog has no static\nheader field. The bridge therefore ships its own minimal authorization server,\nso there is no third-party OAuth app to create. Grok discovers the endpoints via\n`/.well-known/oauth-authorization-server`\n\nand self-registers over RFC 7591.\n\nIf Grok still shows the manual \"OAuth Credentials Required\" form, fill it as:\n\n| Field | Value |\n|---|---|\n| Client ID | anything, e.g. `locum` |\n| Client Secret | leave empty |\n| Authorization Endpoint | `https://<tunnel>/authorize` |\n| Token Endpoint | `https://<tunnel>/token` |\n| Scopes | `mcp` |\n| Token Auth Method | `none (PKCE only)` |\n\nYou'll then get a consent screen. It shows the redirect target -- check it says\n`grok.com`\n\nbefore approving -- and asks for a passphrase: paste your\n`LOCUM_TOKEN`\n\n.\n\nThat passphrase gate is load-bearing. `/authorize`\n\nsits on a public tunnel;\nwithout it, anyone who learned the URL could mint a token and get shell access\nto your machine.\n\nA connector only makes the tools *available*. Without an instruction to prefer\nthem, the Bot keeps grinding through its own loop and you save nothing. Two\nlevers, and the weaker one is the one people reach for first.\n\n**1. The Bot's description (strongest).** Create a dedicated Bot, then\n**Bot actions → Edit Profile → Description**. That field is for rules that\nshould remain true, so it applies to every conversation without being invoked:\n\n```\nYou have the `locum` connector, which delegates work to the operator's own\nmachine.\n\nAny task touching a real repository (multi-file edits, refactors, debugging,\nrunning tests, reading a codebase) must go to delegate_to_claude rather than\nbeing done yourself.\n\ndelegate_to_claude returns a job_id immediately. Poll check_job about every 30s\nand report recent_activity so progress is visible. Never re-delegate a job that\nis still running. For follow-ups on the same work use resume_claude with the\nsession_id, never a fresh delegation.\n\ncwd must be an absolute path inside an allowed root.\n```\n\n**2. A saved Skill (the detail).** `SKILL.md`\n\nin this repo covers how to write a\ngood delegation prompt and what to do when a job errors. Save it by asking a Bot\n\"save this as a skill called delegate-to-locum\" with the file contents pasted,\nthen enable it under **Settings → Plugins → Yours**. Invoke explicitly with `/`\n\nin the composer when you want it applied to a specific task.\n\nUse both. The description guarantees the behaviour; the skill improves the quality of the prompts the Bot writes.\n\n```\npython3 test_oauth.py\n```\n\nBoots a throwaway instance on port 8799 and exercises discovery, dynamic\nregistration, the consent gate, PKCE enforcement, single-use codes, token\nexchange, refresh, and an authenticated MCP `initialize`\n\n. 13 assertions.\n\n```\nuv run --with fastmcp --with uvicorn python3 test_jobs.py\n```\n\nCovers the job registry: ordering, status filtering, limits, truncation, and the\n`LOCUM_MAX_JOBS`\n\ncap. 18 assertions. Neither suite needs `claude`\n\ninstalled.\n\n| Tool | Purpose |\n|---|---|\n`delegate_to_claude(prompt, cwd, model?, effort?)` |\nStart a Claude Code job. Returns `job_id` immediately. |\n`resume_claude(session_id, prompt, cwd?, model?, effort?)` |\nContinue a session. Reuses the prompt cache -- always prefer for follow-ups. |\n`delegate_to_codex(prompt, cwd, model?, effort?)` |\nSame contract, via Codex CLI. |\n`check_job(job_id)` |\nPoll. Returns status, turn count, recent tool activity, result. |\n`list_jobs(limit?, status?)` |\nRecent jobs, newest first. Confirms work really ran, recovers a lost `job_id` , finds a `session_id` to resume. |\n`cancel_job(job_id)` |\nKill a runaway job. |\n\nEverything is async. MCP tool calls time out long before a real coding task\nfinishes, so `delegate_*`\n\nreturns a handle and the Bot polls. This is the single\nthing that makes the integration work at all.\n\n`effort`\n\ntakes one vocabulary across both CLIs, so a caller never has to know\nwhich vendor spells it which way:\n\n`effort` |\nClaude | Codex |\n|---|---|---|\n`low` / `medium` / `high` |\n`--effort <level>` |\n`-c model_reasoning_effort=\"<level>\"` |\n`max` |\n`--effort max` |\n`-c model_reasoning_effort=\"high\"` (no distinct max) |\n\n`model`\n\npasses through unvalidated, since vendors add models faster than any\nallowlist survives. Claude takes aliases (`opus`\n\n, `sonnet`\n\n, `fable`\n\n) or full\nnames; Codex takes its own.\n\nBoth are optional and both cost real quota, so the skill tells the Bot to raise\nthem deliberately: high effort for architecture, subtle debugging, and anything\ntouching auth or data loss, and nothing for mechanical edits. `check_job`\n\nand\n`list_jobs`\n\necho what was actually used.\n\n`https://your-host/dashboard`\n\nshows every delegated session: what was asked,\nwhich model and effort ran it, the full transcript of tool calls, reasoning and\noutput, token counts, cost, and duration. Running jobs stream in live over\nserver-sent events, so it doubles as a window onto work happening right now.\n\nSign in with the same `LOCUM_TOKEN`\n\n. It is exchanged for an HttpOnly cookie, so\nthere is no second secret to manage and revoking the token revokes dashboard\naccess at the same moment. Everything under `/api/`\n\nand `/dashboard`\n\nrequires\nthat cookie.\n\nThis is also the answer to \"how do I show that it is really running on my machine\": the MCP client shows a chat, the dashboard shows the actual tool calls and token spend behind it.\n\nThe server narrates delegated jobs on stdout, so a terminal beside your MCP client shows what is actually running:\n\n``` php\n06:51:24  -> claude  1726b9229609  sonnet  ~/Documents/Projects/locum\n06:51:24       \"How many tools does this MCP server expose? Read server.py...\"\n06:51:30       Bash grep -c \"@mcp.tool\" ...\n06:51:33       Bash grep -n \"@mcp.tool\" ...\n06:51:35  ok claude  1726b9229609  done - 4 turns - 10.3s - $0.17\n```\n\nUnder launchd that goes to `~/Library/Logs/locum/server.out.log`\n\n:\n\n```\ntail -f ~/Library/Logs/locum/server.out.log | grep -v 'INFO:'\n```\n\nSet `LOCUM_NARRATE=0`\n\nfor access logs only. The log has no rotation, so on a\nlong-running install either turn narration off or truncate it periodically.\n\nLocum runs agents autonomously by default, because a delegated job has nobody\nat the keyboard: an approval prompt does not pause the work, it hangs the job\nuntil it times out. `LOCUM_AUTONOMY=bypass`\n\npasses\n`--dangerously-skip-permissions`\n\nto Claude and\n`--dangerously-bypass-approvals-and-sandbox`\n\nto Codex.\n\nBe clear-eyed about what that buys and costs. The agent can run any command as\nyour user. `LOCUM_ROOTS`\n\nbounds the directory a job *starts* in, and it is still\nthe check that stops a caller pointing a job at `~/.ssh`\n\n, but a shell command\nthe agent runs is not confined by it.\n\nWhat actually protects you, in order:\n\n`LOCUM_TOKEN`\n\n, which gates both the consent screen and every MCP call`LOCUM_ROOTS`\n\n, kept narrow- Running this only for yourself, on your own machine\n\n`LOCUM_AUTONOMY=ask`\n\nrestores prompting, but only use it from a client that can\nsurface the prompts. Grok Bot cannot, so jobs will hang.\n\nEvery token comparison uses `hmac.compare_digest`\n\n. Authorization codes are\nsingle-use and expire in 120s. PKCE `S256`\n\nis required -- `plain`\n\nis refused.\n`/health`\n\nand the discovery documents are the only unauthenticated routes;\n`LOCUM_TOKEN`\n\nitself also remains a valid bearer token, which is what makes\n`curl`\n\nsmoke tests work.\n\n**Everything 404s, including /health, but the tunnel says it is connected.**\nPort collision. The Grok Bot desktop app listens on\n\n`[::1]:8787`\n\n, and macOS\nresolves `localhost`\n\nto `::1`\n\nbefore `127.0.0.1`\n\n-- so an ingress pointed at\n`http://localhost:8787`\n\nsilently reaches Grok Bot instead of the bridge, and\nGrok Bot answers `Not found.`\n\nThis is why the default port is **8791** and why the ingress rule uses\n\n`127.0.0.1`\n\n, never `localhost`\n\n. To confirm:\n\n```\nlsof -nPw -iTCP:<port> -sTCP:LISTEN    # who actually owns the port\ncurl -s http://127.0.0.1:<port>/health # locum answers {\"ok\": true}\ncurl -s http://localhost:<port>/health # if this differs, you have a collision\n```\n\n`./demo-port-collision.sh`\n\nreproduces the whole thing in isolation on a port of\nyour choosing, if you want to see the mechanism without waiting to be bitten by\nit.\n\n`cloudflared --loglevel debug tunnel run <name>`\n\nsettles it: each request logs\n`ingressRule=`\n\nand `originService=`\n\n, so you can see whether the 404 came from\ncloudflared's catch-all or from whatever is actually on the port.\n\n**Jobs fail with \"OAuth session expired and could not be refreshed\".** Check the\nboring cause first: run `claude -p \"reply with OK\"`\n\nyourself. If that fails too,\nyour Claude Code login has genuinely expired and `claude /login`\n\nfixes it. Locum\nsurfaces the CLI's error verbatim, so this looks identical to a Locum bug.\n\nIf `claude -p`\n\nworks standalone but fails through Locum, then the\nserver was launched from inside a Claude Code session. Claude Code exports\n`CLAUDECODE`\n\n, a `CLAUDE_CODE_*`\n\nfamily, and `ANTHROPIC_BASE_URL`\n\ninto every\nchild process; a `claude`\n\nthat inherits them believes it is a nested child\nsession and tries to delegate auth to a host socket that is not listening.\n`_child_env()`\n\nstrips those when nesting is detected, but some sandboxed hosts\nbroker Claude's credentials entirely in-process, and there a spawned `claude`\n\nhas nothing on disk to authenticate with no matter what the environment says.\nRun the server from an ordinary terminal.\n\n**Cloudflare returns 403 with error code: 1010.** Cloudflare bans the default\n\n`Python-urllib`\n\nUser-Agent signature. Only that signature -- curl, Go, Node,\nokhttp, and an absent User-Agent all pass, so MCP clients are unaffected. Set a\nUser-Agent on any Python tooling you point at the tunnel:\n\n```\nurllib.request.Request(url, headers={\"User-Agent\": \"locum-check/1.0\"})\n```\n\n- Cuts Grok Bot usage, does not zero it -- orchestration turns still meter. The win is collapsing ~50 Bot steps into one tool call plus a few polls.\n- Your machine must be awake with the tunnel up.\n- Quick-tunnel URLs change on restart;\n`./setup-tunnel.sh`\n\ngives you a stable hostname so the connector survives. - Cold delegation re-pays ~18k tokens of\n`CLAUDE.md`\n\n+ system prompt setup.`resume_claude`\n\navoids it.\n\n[How it works](https://www.harjotrana.com/blog/locum-grok-bot-provider-adapter), the architecture writeup, with diagrams and the three bugs that cost the most time[CONTRIBUTING.md](/HarjjotSinghh/locum/blob/main/CONTRIBUTING.md), development setup and the four invariants[SECURITY.md](/HarjjotSinghh/locum/blob/main/SECURITY.md), threat model and how to report a vulnerability. Read this before exposing Locum to a tunnel[CHANGELOG.md](/HarjjotSinghh/locum/blob/main/CHANGELOG.md)\n\nLicensed under [Apache 2.0](/HarjjotSinghh/locum/blob/main/LICENSE).", "url": "https://wpnews.pro/news/show-hn-locum-grok-bot-delegates-tasks-to-claude-codex-cli-on-your-machine", "canonical_source": "https://github.com/HarjjotSinghh/locum", "published_at": "2026-08-22 20:41:34+00:00", "updated_at": "2026-08-22 21:13:57.991521+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents", "ai-infrastructure"], "entities": ["Locum", "Grok Bot", "Claude Code", "Codex CLI", "xAI", "Anthropic", "OpenAI", "Cloudflare"], "alternates": {"html": "https://wpnews.pro/news/show-hn-locum-grok-bot-delegates-tasks-to-claude-codex-cli-on-your-machine", "markdown": "https://wpnews.pro/news/show-hn-locum-grok-bot-delegates-tasks-to-claude-codex-cli-on-your-machine.md", "text": "https://wpnews.pro/news/show-hn-locum-grok-bot-delegates-tasks-to-claude-codex-cli-on-your-machine.txt", "jsonld": "https://wpnews.pro/news/show-hn-locum-grok-bot-delegates-tasks-to-claude-codex-cli-on-your-machine.jsonld"}}