Feature Request: claude-code provider that shells out to claude -p for Max/Pro subscription quota
Summary
Add a first-class claude-code provider to Hermes that routes requests through the local claude CLI (Claude Code) instead of the native Anthropic Messages API. This unblocks Claude Max / Pro subscribers whose OAuth flow currently drains "extra usage" credits via the third-party gateway instead of drawing from the subscription quota.
Problem
The Anthropic OAuth provider in Hermes is unusable for Claude Max / Pro subscribers:
- Issue #40014 (open, P2): even with valid credentials from
~/.claude/.credentials.json, Hermes' Anthropic OAuth path still hits the pay-per-token api.anthropic.com endpoint and burns the subscriber's "extra usage" balance instead of using the bundled Max/Pro quota. The 4.6 mcp__ prefix fix (PR f9c8d95) addressed one symptom but did not change the routing — the underlying OAuth bill flow is broken.
- Issue #15080 (closed "Not Planned"): HTTP 400 from
api.anthropic.com/v1/messages when the tools parameter is present on a Claude Max OAuth token.
- Issue #29125 (closed "Not Planned"): "[Bug]: Hermes does not work through Claude CLI" — community member
pokono explicitly suggested routing through claude -p ("This is what happened with the claw, just unreliable"). The thread confirms other subscribers (arksnorman, sedhasukhdeep) have given up and switched to Codex.
ToS and account-ban risk: routing subscription OAuth through a third-party HTTP client is what got the OpenClaw creator banned on 2026-04-04. Anthropic is actively enforcing against this pattern. Continuing to recommend the current OAuth path exposes users to the same risk.
Proposed solution: claude-code provider
Mirror the architectural pattern already shipped for copilot-acp (agent/copilot_acp_client.py) but spawn claude instead of copilot.
What it would look like from the user side
# Prerequisites: `claude` CLI in PATH with an active `claude login` session
hermes chat --provider claude-code --model claude-sonnet-4-6 "explain this codebase"
hermes model # claude-code appears in the picker
hermes setup # wizard detects `claude` in PATH and offers it as an option
# config.yaml (non-secret — per project contribution guidelines,
# behavioral settings belong here, not in .env)
providers:
claude-code:
daily_spend_cap_usd: 25.0 # hard cap, raises clear error
permission_mode: bypass # --dangerously-skip-permissions for non-interactive
max_session_idle_seconds: 900 # reap stale stdio processes
fallback_models:
- claude-sonnet-4-6
- claude-opus-4-7
Architectural template
| Concern |
copilot-acp precedent |
claude-code adaptation |
| Subprocess transport |
copilot --acp --stdio (JSON-RPC) |
claude -p --output-format stream-json --verbose --dangerously-skip-permissions --resume <sid> (JSONL over stdio) |
| Lifecycle |
one process per request |
one process per Hermes session, resumed by Hermes session ID — eliminates turn-2+ startup latency, enables true multi-turn context |
| Tool calls |
JSON-RPC structured |
regex-extracted from text (<tool_call>{...}</tool_call> blocks); tool schemas injected into system prompt |
| Auth |
copilot login token in subprocess env |
Claude Code credentials in ~/.claude/.credentials.json, picked up by claude itself |
| Streaming |
disabled (SimpleNamespace return) |
disabled for claude://code base URL — same pattern as the acp://copilot exclusion at conversation_loop.py:1097 |
| Spend visibility |
n/a |
parse final JSONL event for total_cost_usd, surface via the same hook the Anthropic adapter already exposes; integrated with daily_spend_cap_usd config |
Integration surface (mirrors copilot-acp 1:1)
plugins/model-providers/claude-code/__init__.py — register_provider(ClaudeCodeProfile(...)) with auth_type="external_process", base_url_override="claude://code"
plugins/model-providers/claude-code/plugin.yaml — manifest
agent/claude_code_client.py — adapter (~600 LOC, ports _ACPChatCompletions / CopilotACPClient to stream-json parsing + persistent stdio + spend tracking)
agent/auxiliary_client.py — extend the auth_type == "external_process" branch at line ~4047 to construct ClaudeCodeClient for provider id claude-code
agent/agent_runtime_helpers.py:1365 — extend the provider == "copilot-acp" branch to also match claude-code
agent/conversation_loop.py:1097 — extend the streaming exclusion branch to also match claude://code
hermes_cli/providers.py:91 — add claude-code entry to HERMES_OVERLAYS with auth_type="external_process", transport="codex_responses" (re-use the no-streaming transport)
hermes_cli/auth.py:92 — add DEFAULT_CLAUDE_CODE_BASE_URL = "claude://code"
website/docs/integrations/providers.md — add a section parallel to the existing copilot-acp section (line ~192)
tests/agent/test_claude_code_client.py — mirror the copilot ACP test surface (permission handling, prompt caching safety, cost extraction from the final JSONL event)
Why this is ToS-safe
Per OpenClaw's docs (docs.openclaw.ai/providers/claude-max-api-proxy and docs.openclaw.ai/gateway/cli-backends): "Anthropic staff told us OpenClaw-style CLI usage is allowed again." The claude CLI is Anthropic's own first-party client. Hermes spawning it as a subprocess does not inject itself as a third-party HTTP client and does not require the Authorization: Bearer ... header that triggers the third-party billing path.
This is a strict superset of what users can already do manually: open a terminal, run claude, paste the prompt. The provider just automates the existing Anthropic-blessed workflow.
Key design decisions I want maintainer feedback on
-
api_mode naming: copilot-acp uses "chat_completions" with a comment saying the subprocess uses chat-completions routing. Is there appetite to introduce a cleaner external_process transport now that we'd have two consumers, or should claude-code keep piggybacking on codex_responses?
-
Per-session persistent stdio vs stateless per-request: OpenClaw's claude-cli backend keeps one claude process alive per session and uses --resume <sid> for turns 2+. This is the right architecture (zero startup latency, true multi-turn context) but introduces a process-lifecycle question: how does it interact with run_agent.py's interrupt / rebuild / fallback paths? Open to a simpler v1 (stateless, like copilot-acp) if the persistent-stdio version is too speculative.
-
daily_spend_cap_usd placement: I'm proposing config.yaml because the contribution guidelines say non-secret config belongs there. But "spend" feels closer to a credential-adjacent concern. Happy to move to .env if maintainers prefer — or to drop it from v1 entirely if it bloats the PR.
-
run_oauth_setup_token() skeleton at agent/anthropic_adapter.py: I noticed there's an unused subprocess helper in the adapter. Is the maintainer team open to wiring it up as a fallback for users who don't have the claude CLI installed? Or is that out of scope for this issue?
Precedent: OpenClaw
OpenClaw already ships both architectures in production:
claude-max-api-proxy (docs): local OpenAI-compatible server (default localhost:3456/v1/chat/completions) that converts requests to claude -p CLI calls. Stateless per-request, like the personal proxy some users have built locally.
claude-cli (docs): keeps a claude stdio process alive per session via stream-json stdin. Proper multi-turn state, zero startup latency on turns 2+. This is the architecture being proposed here.
Migration writeup: Switching OpenClaw to CLI backends for Claude and Codex.
Implementation gotchas I want documented up front
These will all need to land in the implementation but listing them so reviewers can pre-empt bikesheds:
--dangerously-skip-permissions is mandatory — without it, claude stalls on permission prompts in non-interactive mode. The provider would error out cleanly with a setup hint if the user explicitly opts out.
- Tool calls come embedded in text, not as JSON-RPC —
claude -p does not return structured tool calls. The adapter must inject tool definitions into the system prompt and regex-extract responses (same _TOOL_CALL_BLOCK_RE / _TOOL_CALL_JSON_RE patterns that copilot_acp_client.py already uses).
--resume lookup key — should be the Hermes session ID, not a content hash. Content-hash keying (what some local proxies do) breaks the moment the user changes a message in the middle of the conversation; Hermes' session ID is stable.
- Spend tracking —
total_cost_usd only appears in the final JSONL event when --output-format stream-json --verbose is set. Mid-stream events have usage but not cost. The adapter needs to buffer the cost value until the final event and surface it on the response object's usage attribute for parity with the Anthropic adapter.
Willingness to implement
I'm happy to draft the PR — the copilot-acp template is direct enough that I can clone the pattern with confidence. But given that #29125 and #15080 were both closed "Not Planned" before the ToS ban enforcement became public, I'd rather get a maintainer's thumbs-up on the architecture before sinking 1500 LOC into a PR that might get closed the same way.
Specifically I'd like signal on:
- Is the
external_process auth_type the right home, or should this live in hermes_cli/providers.py as a transport overlay (like copilot-acp currently does)?
- Is the per-session persistent stdio design worth the lifecycle complexity, or should v1 ship stateless and treat stdio-alive as a follow-up?
- Is
daily_spend_cap_usd in scope for this PR or a separate issue?
Once I have a yes/no on those, I can have a PR ready in a day or two.
Related issues
Feature Request:
claude-codeprovider that shells out toclaude -pfor Max/Pro subscription quotaSummary
Add a first-class
claude-codeprovider to Hermes that routes requests through the localclaudeCLI (Claude Code) instead of the native Anthropic Messages API. This unblocks Claude Max / Pro subscribers whose OAuth flow currently drains "extra usage" credits via the third-party gateway instead of drawing from the subscription quota.Problem
The Anthropic OAuth provider in Hermes is unusable for Claude Max / Pro subscribers:
~/.claude/.credentials.json, Hermes' Anthropic OAuth path still hits the pay-per-tokenapi.anthropic.comendpoint and burns the subscriber's "extra usage" balance instead of using the bundled Max/Pro quota. The 4.6 mcp__ prefix fix (PR f9c8d95) addressed one symptom but did not change the routing — the underlying OAuth bill flow is broken.api.anthropic.com/v1/messageswhen thetoolsparameter is present on a Claude Max OAuth token.pokonoexplicitly suggested routing throughclaude -p("This is what happened with the claw, just unreliable"). The thread confirms other subscribers (arksnorman, sedhasukhdeep) have given up and switched to Codex.ToS and account-ban risk: routing subscription OAuth through a third-party HTTP client is what got the OpenClaw creator banned on 2026-04-04. Anthropic is actively enforcing against this pattern. Continuing to recommend the current OAuth path exposes users to the same risk.
Proposed solution:
claude-codeproviderMirror the architectural pattern already shipped for
copilot-acp(agent/copilot_acp_client.py) but spawnclaudeinstead ofcopilot.What it would look like from the user side
Architectural template
copilot-acpprecedentclaude-codeadaptationcopilot --acp --stdio(JSON-RPC)claude -p --output-format stream-json --verbose --dangerously-skip-permissions --resume <sid>(JSONL over stdio)<tool_call>{...}</tool_call>blocks); tool schemas injected into system promptcopilot logintoken in subprocess env~/.claude/.credentials.json, picked up byclaudeitselfclaude://codebase URL — same pattern as theacp://copilotexclusion atconversation_loop.py:1097total_cost_usd, surface via the same hook the Anthropic adapter already exposes; integrated withdaily_spend_cap_usdconfigIntegration surface (mirrors
copilot-acp1:1)plugins/model-providers/claude-code/__init__.py—register_provider(ClaudeCodeProfile(...))withauth_type="external_process",base_url_override="claude://code"plugins/model-providers/claude-code/plugin.yaml— manifestagent/claude_code_client.py— adapter (~600 LOC, ports_ACPChatCompletions/CopilotACPClientto stream-json parsing + persistent stdio + spend tracking)agent/auxiliary_client.py— extend theauth_type == "external_process"branch at line ~4047 to constructClaudeCodeClientfor provider idclaude-codeagent/agent_runtime_helpers.py:1365— extend theprovider == "copilot-acp"branch to also matchclaude-codeagent/conversation_loop.py:1097— extend the streaming exclusion branch to also matchclaude://codehermes_cli/providers.py:91— addclaude-codeentry toHERMES_OVERLAYSwithauth_type="external_process",transport="codex_responses"(re-use the no-streaming transport)hermes_cli/auth.py:92— addDEFAULT_CLAUDE_CODE_BASE_URL = "claude://code"website/docs/integrations/providers.md— add a section parallel to the existingcopilot-acpsection (line ~192)tests/agent/test_claude_code_client.py— mirror the copilot ACP test surface (permission handling, prompt caching safety, cost extraction from the final JSONL event)Why this is ToS-safe
Per OpenClaw's docs (
docs.openclaw.ai/providers/claude-max-api-proxyanddocs.openclaw.ai/gateway/cli-backends): "Anthropic staff told us OpenClaw-style CLI usage is allowed again." TheclaudeCLI is Anthropic's own first-party client. Hermes spawning it as a subprocess does not inject itself as a third-party HTTP client and does not require theAuthorization: Bearer ...header that triggers the third-party billing path.This is a strict superset of what users can already do manually: open a terminal, run
claude, paste the prompt. The provider just automates the existing Anthropic-blessed workflow.Key design decisions I want maintainer feedback on
api_mode naming: copilot-acp uses
"chat_completions"with a comment saying the subprocess uses chat-completions routing. Is there appetite to introduce a cleanerexternal_processtransport now that we'd have two consumers, or shouldclaude-codekeep piggybacking oncodex_responses?Per-session persistent stdio vs stateless per-request: OpenClaw's
claude-clibackend keeps oneclaudeprocess alive per session and uses--resume <sid>for turns 2+. This is the right architecture (zero startup latency, true multi-turn context) but introduces a process-lifecycle question: how does it interact withrun_agent.py's interrupt / rebuild / fallback paths? Open to a simpler v1 (stateless, likecopilot-acp) if the persistent-stdio version is too speculative.daily_spend_cap_usdplacement: I'm proposingconfig.yamlbecause the contribution guidelines say non-secret config belongs there. But "spend" feels closer to a credential-adjacent concern. Happy to move to.envif maintainers prefer — or to drop it from v1 entirely if it bloats the PR.run_oauth_setup_token()skeleton atagent/anthropic_adapter.py: I noticed there's an unused subprocess helper in the adapter. Is the maintainer team open to wiring it up as a fallback for users who don't have theclaudeCLI installed? Or is that out of scope for this issue?Precedent: OpenClaw
OpenClaw already ships both architectures in production:
claude-max-api-proxy(docs): local OpenAI-compatible server (defaultlocalhost:3456/v1/chat/completions) that converts requests toclaude -pCLI calls. Stateless per-request, like the personal proxy some users have built locally.claude-cli(docs): keeps aclaudestdio process alive per session viastream-jsonstdin. Proper multi-turn state, zero startup latency on turns 2+. This is the architecture being proposed here.Migration writeup: Switching OpenClaw to CLI backends for Claude and Codex.
Implementation gotchas I want documented up front
These will all need to land in the implementation but listing them so reviewers can pre-empt bikesheds:
--dangerously-skip-permissionsis mandatory — without it,claudestalls on permission prompts in non-interactive mode. The provider would error out cleanly with a setup hint if the user explicitly opts out.claude -pdoes not return structured tool calls. The adapter must inject tool definitions into the system prompt and regex-extract responses (same_TOOL_CALL_BLOCK_RE/_TOOL_CALL_JSON_REpatterns thatcopilot_acp_client.pyalready uses).--resumelookup key — should be the Hermes session ID, not a content hash. Content-hash keying (what some local proxies do) breaks the moment the user changes a message in the middle of the conversation; Hermes' session ID is stable.total_cost_usdonly appears in the final JSONL event when--output-format stream-json --verboseis set. Mid-stream events have usage but not cost. The adapter needs to buffer the cost value until the final event and surface it on the response object'susageattribute for parity with the Anthropic adapter.Willingness to implement
I'm happy to draft the PR — the copilot-acp template is direct enough that I can clone the pattern with confidence. But given that #29125 and #15080 were both closed "Not Planned" before the ToS ban enforcement became public, I'd rather get a maintainer's thumbs-up on the architecture before sinking 1500 LOC into a PR that might get closed the same way.
Specifically I'd like signal on:
external_processauth_type the right home, or should this live inhermes_cli/providers.pyas a transport overlay (likecopilot-acpcurrently does)?daily_spend_cap_usdin scope for this PR or a separate issue?Once I have a yes/no on those, I can have a PR ready in a day or two.
Related issues