Skip to content

Use the Claude subscription through the official Claude Code CLI (ported from Hermes) - #101

Merged
arefbehboudi merged 2 commits into
ClawRunr:mainfrom
iNicholasBE:claude-subscription-via-claude-code
Oct 27, 2026
Merged

arefbehboudi merged 2 commits into
ClawRunr:mainfrom
iNicholasBE:claude-subscription-via-claude-code

Conversation

@iNicholasBE

Copy link
Copy Markdown
Contributor

Why

JavaClaw's Claude Code support read the OAuth token straight out of the macOS Keychain (or ~/.claude/.credentials.json) and called the API itself, impersonating Claude Code with its beta headers and system-prompt prefix. Our own onboarding page warned that this "violates the terms of your subscription and may result in account suspension".

What

This PR replaces that with a port of the Hermes Claude Subscription DirectSDK provider (NousResearch/hermes-official-plugins, MIT). JavaClaw never reads or sends the subscription credential. Everything goes through the official claude CLI, which handles login, token refresh and authentication itself.

How it works (new providers/claude-code module, provider type claude-code):

  • One CLI process per call: each model call starts a fresh claude -p process in stream-json mode. Native tools, skills, settings, memory and compaction are off, and --max-turns 1 limits it to one turn.
  • Replay: the conversation is replayed as stream-json frames, and Claude Code generates exactly one assistant turn.
  • JavaClaw keeps the agent loop: tool calls come back as normal Spring AI tool calls, so JavaClaw's own ToolCallingAdvisor runs the tools.
  • Single-request relay: a request-scoped loopback AdmissionRelay is Claude Code's ANTHROPIC_BASE_URL. It forwards only the first Messages request upstream and denies Claude Code's own retries locally, so one JavaClaw call can never become several billed requests.
  • Prompt-cache hygiene: RelayPayloads keeps the cached prefix stable across tool rounds. It fails open, ported from the Hermes admission transforms.
  • Signed thinking replay: Claude Code's native assistant messages, signed thinking included, ride along in assistant-message metadata so unchanged turns replay byte for byte.
  • Fail closed: inherited ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL / Bedrock / Vertex overrides are refused rather than silently billing an API key, and a custom base URL is rejected.

Deviations from Hermes:

  • Hermes advertises the tool inventory through a Python stdio MCP process. Here the relay serves it over loopback HTTP, which avoids a second process per call; it's verified that the CLI connects (initialize, tools/list).
  • Not ported: request gzip, HTTPS proxy support in the relay, the live model picker, /usage plan limits, the telemetry setting, and reasoning-effort / structured-output passthrough.

Onboarding:

  • AgentOnboardingProvider drops systemWideToken() and gains readinessProblem(), supportsBaseUrl() and models().
  • The "Claude subscription" step checks claude auth status before saving, hides the base URL field, and shows the models as a dropdown. Full ids are stored, and a saved model that isn't in the list stays selectable.

Bug fix found while testing:

  • The in-process restart after onboarding re-read classpath:application.private.yaml, which is a stale build copy when running from Gradle or the IDE. That left the app on the "no model configured" placeholder, which fails with streaming is not supported.
  • application.yaml now also imports the file ConfigurationManager actually writes, so the restart picks up the new config.

Migration: a config that still holds api-key: <claude-code-bearer-token> under provider: anthropic now fails with a clear message. Switch it to provider: claude-code and remove the api-key.

Testing

  • ClaudeCodeChatModelIntegrationTest drives the real claude CLI (2.1.287) against a fake loopback Anthropic upstream: no credentials, no inference. It covers text, tool calls, history replay with signed thinking, the full ChatClient + ToolCallingAdvisor loop (blocking and streaming), upstream error mapping (429), truncation and cancellation. It's skipped when claude isn't installed.
  • RelayPayloadsTest holds 29 cases ported from the Hermes admission tests. ClaudeCodeRequestTest, SseCaptureTest, ClaudeCodeCliTest, ClaudeCodeModelsTest and three new OnboardingControllerTest cases cover the rest.
  • Live: a tool round trip on a Max subscription through ChatClient + ToolCallingAdvisor worked. Onboarding and web chat were also tested by hand.
  • Results: claude-code 67, anthropic 2 and app 49 tests pass, including the ArchUnit provider-independence rules.

Note: when Gradle runs on JDK 27, :ui:vaadinBuildFrontend fails with "Unsupported class file major version 71". That is unrelated to this PR; run Gradle on JDK 25.

iNicholasBE and others added 2 commits October 26, 2026 12:09
…ted from Hermes

Replaces the old Claude Code integration, which read the OAuth token from the
Keychain / ~/.claude/.credentials.json and called the API with Claude Code's
headers and system prompt. The new providers/claude-code module is a Java port
of the Hermes "Claude Subscription DirectSDK" provider
(NousResearch/hermes-official-plugins, MIT): each model call runs a fresh
`claude -p` process, a loopback relay admits exactly one upstream request and
keeps the prompt cache stable, and JavaClaw's own ToolCallingAdvisor keeps the
agent loop. JavaClaw never reads or sends the subscription credential.

Onboarding checks `claude auth status`, offers the Claude models as a
dropdown, and the restart after onboarding now reads the config file
ConfigurationManager writes instead of a stale classpath copy.
Switching the default provider kept the previous provider's base-url and
api-key in the config file, and the credentials step prefilled the previous
provider's model, so moving to the Claude subscription could end with a
base URL it rejects and a model it cannot run. ConfigurationManager now
removes a key passed with a null value, onboarding uses that to drop unused
settings, and saved values only prefill the form for the same provider.

The private config is now imported only from the file ConfigurationManager
writes, so a removed key no longer comes back from the stale classpath copy.
@arefbehboudi
arefbehboudi self-requested a review October 27, 2026 06:02
@arefbehboudi

Copy link
Copy Markdown
Collaborator

Thanks @iNicholasBE, That's what I really need it 😄 💯

@arefbehboudi
arefbehboudi merged commit faf38cf into ClawRunr:main Oct 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants