Skip to content

Add standalone Claude CLI provider for local headless usage#61160

Closed
AlexManev wants to merge 3 commits into
openclaw:mainfrom
AlexManev:main
Closed

Add standalone Claude CLI provider for local headless usage#61160
AlexManev wants to merge 3 commits into
openclaw:mainfrom
AlexManev:main

Conversation

@AlexManev

Copy link
Copy Markdown

Summary

  • Problem: Anthropic has blocked OAuth for third-party use, making direct API key and OAuth flows unavailable for self-hosted OpenClaw deployments that rely solely on a locally authenticated Claude CLI installation.
  • Why it matters: Users with a working claude auth login session have no first-class path to use OpenClaw without separately obtaining an Anthropic API key.
  • What changed: Added a new claude-headless bundled plugin that registers a standalone CLI backend (claude -p --permission-mode bypassPermissions) and a matching provider plugin with a credential-detection auth method — no API key required.
  • What did NOT change: The existing anthropic provider, the claude-cli backend registered by that extension, the Anthropic API transport, and all other provider plugins are untouched.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #
  • Related #
  • This PR fixes a bug or regression

Root Cause (if applicable)

N/A — this is a new feature, not a bug fix.

  • Root cause: N/A
  • Missing detection / guardrail: N/A
  • Contributing context (if known): Anthropic blocking OAuth for third-party integrations leaves users with a locally-authenticated Claude CLI and no direct API key with no supported inference path in OpenClaw.

Regression Test Plan (if applicable)

N/A — new plugin with no existing coverage to regress.

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: N/A
  • Scenario the test should lock in: N/A
  • Why this is the smallest reliable guardrail: N/A
  • Existing test that already covers this (if any): The existing cli-runner and cli-backends test suites cover the underlying backend execution path; the new backend follows the same pattern as the existing claude-cli backend.
  • If no new test is added, why not: The auth detection logic delegates entirely to the existing and already-tested readClaudeCliCredentialsCached. The backend config is a static struct with no branching logic to unit-test independently.

User-visible / Behavior Changes

  • New provider claude-headless appears in openclaw auth provider picker as “Claude Headless” group with choice “Claude headless (local CLI)”.
  • Running openclaw auth and selecting that choice sets agents.defaults.model to claude-headless/claude-sonnet-4-6 — no API key prompt.
  • Models claude-headless/claude-sonnet-4-6, claude-headless/claude-opus-4-6, claude-headless/claude-opus-4-5, claude-headless/claude-sonnet-4-5, claude-headless/claude-haiku-4-5 become available as model refs.
  • All other defaults and providers are unchanged.

Diagram (if applicable)

For UI changes or non-trivial logic flows, include a small ASCII diagram reviewers can scan quickly. Otherwise write N/A.


Before:
[user, no API key] -> openclaw auth -> Anthropic group only -> blocked (no key / OAuth blocked)

After:
[user, no API key] -> openclaw auth -> "Claude Headless" group
                   -> detect local claude credentials
                   -> set model = claude-headless/claude-sonnet-4-6
                   -> inference: spawn claude -p --permission-mode bypassPermissions

Security Impact (required)

  • New permissions/capabilities? Yes — the backend spawns claude with --permission-mode bypassPermissions, which allows the subprocess to execute arbitrary tools without per-call permission prompts. This is intentional and mirrors exactly what the existing claude-cli backend already does. Users who run this backend are explicitly opting into the same trust model as the existing CLI backend.
  • Secrets/tokens handling changed? No — the extension reads existing Claude CLI credentials via the public readClaudeCliCredentialsCached helper (no new credential storage). It also clears ANTHROPIC_API_KEY from the subprocess environment to prevent accidental key exposure to the child process.
  • New/changed network calls? No — all network calls are made by the child claude process using its own stored credentials, not by OpenClaw directly.
  • Command/tool execution surface changed? Yes — a new CLI backend subprocess path is registered. The command is claude (the official Claude CLI binary), the same binary used by the existing claude-cli backend.
  • Data access scope changed? No
  • Risk + mitigation: The bypassPermissions flag gives the spawned Claude process broad tool access. This is the same risk profile as the existing claude-cli backend in the anthropic extension; no new attack surface is introduced. Users must have already run claude auth login and explicitly chosen this provider.

Repro + Verification

Environment

  • OS: Linux x64
  • Runtime/container: Node 22 / pnpm workspace
  • Model/provider: claude-headless/claude-sonnet-4-6
  • Integration/channel (if any): N/A
  • Relevant config (redacted):
agents:
  defaults:
    model: claude-headless/claude-sonnet-4-6

Steps

  1. Authenticate the local Claude CLI: claude auth login
  2. Run openclaw auth and select Claude Headless → Claude headless (local CLI)
  3. Send a message through any configured channel

Expected

  • Setup completes without prompting for an API key
  • agents.defaults.model is set to claude-headless/claude-sonnet-4-6
  • Inference routes through the local claude process

Actual

  • Same as expected

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: Plugin loads without errors; pnpm check (type check + lint) passes; extension manifest is structurally consistent with other bundled provider plugins (e.g. anthropic, kilocode).
  • Edge cases checked: Legacy --dangerously-skip-permissions in user config overrides is normalized to --permission-mode bypassPermissions by normalizeBackendConfig; ANTHROPIC_API_KEY is cleared from subprocess env.
  • What you did not verify: Behavior when claude is not installed or not on PATH (the auth method throws a clear error message in that case via hasClaudeHeadlessAuth()).

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? (Yes) opt-in only, no existing behavior changed.
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

List only real risks for this PR. Add/remove entries as needed. If none, write None.

  • Risk: User confusion between claude-cli/* (from the anthropic extension migration) and claude-headless/* model refs.
    • Mitigation: The two use distinct provider IDs and are surfaced through separate auth choices. claude-cli/* is the result of migrating an existing Anthropic auth; claude-headless/* is the explicit standalone no-key path.

claude and others added 3 commits April 4, 2026 22:10
Adds a new claude-headless extension that routes inference through the
local Claude CLI binary (claude -p --permission-mode bypassPermissions)
instead of the Anthropic API. This lets users who cannot use Anthropic
OAuth or API keys run OpenClaw using a locally authenticated Claude CLI
installation.

The extension registers:
- A claude-headless CLI backend that spawns claude in headless/print
  mode with bypassPermissions (the modern replacement for the legacy
  --dangerously-skip-permissions flag).
- A claude-headless provider plugin with a detect auth method that
  reads local Claude CLI credentials via readClaudeCliCredentialsCached
  and sets agents.defaults.model = claude-headless/claude-sonnet-4-6.

Users set up via: openclaw auth (select "Claude headless (local CLI)")
or manually set agents.defaults.model to a claude-headless/* ref.
feat(claude-headless): add standalone headless Claude CLI provider
@greptile-apps

greptile-apps Bot commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

New extensions/claude-headless plugin adds a standalone headless Claude CLI provider (no API key) by registering a claude -p --permission-mode bypassPermissions backend and a credential-detection auth method. The implementation is structurally sound and mirrors the anthropic extension's existing CLI backend, but significantly duplicates shared logic (normalizePermissionArgs, MODEL_ALIASES, SESSION_ID_FIELDS, CLEAR_ENV) already defined in extensions/anthropic/cli-shared.ts, in violation of the extension boundary guide's explicit "extract, don't copy" policy.

  • The duplicated normalization/alias/constants logic must be promoted to an openclaw/plugin-sdk subpath so both extensions share a single source of truth — the boundary guide requires this to happen in the same change, not deferred.

Confidence Score: 4/5

Mostly safe to merge, but a P1 architectural violation (policy-required shared-helper extraction) should be resolved first.

One P1 finding keeps this from a 5: the duplicated normalization/alias logic violates the extension boundary guide's explicit requirement to extract shared helpers in the same change rather than copy them. The logic itself is correct and mirrors the existing anthropic backend precisely; the risk is future silent divergence when the upstream helpers evolve. The P2 missing haiku alias is low-severity since the Claude CLI may accept the full model name gracefully.

extensions/claude-headless/index.ts — shared logic duplication and missing haiku-4-5 alias.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/claude-headless/index.ts
Line: 22-118

Comment:
**Duplicated shared CLI logic violates extension boundary policy**

`normalizePermissionArgs` (lines 78–110), `MODEL_ALIASES` (lines 22–42), `SESSION_ID_FIELDS` (line 44), and `CLEAR_ENV` (line 48) are all verbatim copies of the identically-named constants already exported from `extensions/anthropic/cli-shared.ts`. The extension boundary guide in `extensions/CLAUDE.md` explicitly states: _"If two bundled providers share the same replay policy shape… stop copying the logic. Extract one shared helper and migrate both call sites in the same change."_ Cross-extension imports are prohibited, so the correct fix is to promote these to a public `openclaw/plugin-sdk` subpath (e.g. `openclaw/plugin-sdk/cli-backend`) and import from there in both extensions. As-is, any bug fix to `normalizeClaudePermissionArgs` in the anthropic extension silently leaves this extension diverged.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/claude-headless/index.ts
Line: 39-42

Comment:
**`claude-haiku-4-5` missing from `MODEL_ALIASES`**

The wizard `modelAllowlist.allowedKeys` includes `"claude-headless/claude-haiku-4-5"`, but `MODEL_ALIASES` has no entry for `haiku-4-5` or `claude-haiku-4-5` (only `haiku-3.5` / `claude-haiku-3-5`). After stripping the provider prefix, the alias lookup silently returns `undefined` and the CLI receives `--model claude-haiku-4-5` verbatim instead of the `haiku` shorthand. Add the missing entries:

```suggestion
  haiku: "haiku",
  "haiku-3.5": "haiku",
  "haiku-4.5": "haiku",
  "claude-haiku-3-5": "haiku",
  "claude-haiku-4-5": "haiku",
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Merge branch 'openclaw:main' into main" | Re-trigger Greptile

Comment on lines +22 to +118
const MODEL_ALIASES: Record<string, string> = {
opus: "opus",
"opus-4.6": "opus",
"opus-4.5": "opus",
"opus-4": "opus",
"claude-opus-4-6": "opus",
"claude-opus-4-5": "opus",
"claude-opus-4": "opus",
sonnet: "sonnet",
"sonnet-4.6": "sonnet",
"sonnet-4.5": "sonnet",
"sonnet-4.1": "sonnet",
"sonnet-4.0": "sonnet",
"claude-sonnet-4-6": "sonnet",
"claude-sonnet-4-5": "sonnet",
"claude-sonnet-4-1": "sonnet",
"claude-sonnet-4-0": "sonnet",
haiku: "haiku",
"haiku-3.5": "haiku",
"claude-haiku-3-5": "haiku",
};

const SESSION_ID_FIELDS = ["session_id", "sessionId", "conversation_id", "conversationId"] as const;

// Env vars to clear so the local Claude CLI uses its own stored credentials,
// not a direct API key that might route to a different account.
const CLEAR_ENV = ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"] as const;

// Headless mode args: -p (print/headless), stream-json output, and
// bypassPermissions replaces the legacy --dangerously-skip-permissions flag.
const HEADLESS_ARGS = [
"-p",
"--output-format",
"stream-json",
"--include-partial-messages",
"--verbose",
"--permission-mode",
"bypassPermissions",
] as const;

const HEADLESS_RESUME_ARGS = [
"-p",
"--output-format",
"stream-json",
"--include-partial-messages",
"--verbose",
"--permission-mode",
"bypassPermissions",
"--resume",
"{sessionId}",
] as const;

/**
* Normalizes legacy --dangerously-skip-permissions to the modern
* --permission-mode bypassPermissions in user-supplied config overrides.
*/
function normalizePermissionArgs(args?: string[]): string[] | undefined {
if (!args) {
return args;
}
const normalized: string[] = [];
let sawLegacySkip = false;
let hasPermissionMode = false;
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === "--dangerously-skip-permissions") {
sawLegacySkip = true;
continue;
}
if (arg === "--permission-mode") {
hasPermissionMode = true;
normalized.push(arg);
const maybeValue = args[i + 1];
if (typeof maybeValue === "string") {
normalized.push(maybeValue);
i += 1;
}
continue;
}
if (arg.startsWith("--permission-mode=")) {
hasPermissionMode = true;
}
normalized.push(arg);
}
if (sawLegacySkip && !hasPermissionMode) {
normalized.push("--permission-mode", "bypassPermissions");
}
return normalized;
}

function normalizeBackendConfig(config: CliBackendConfig): CliBackendConfig {
return {
...config,
args: normalizePermissionArgs(config.args),
resumeArgs: normalizePermissionArgs(config.resumeArgs),
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Duplicated shared CLI logic violates extension boundary policy

normalizePermissionArgs (lines 78–110), MODEL_ALIASES (lines 22–42), SESSION_ID_FIELDS (line 44), and CLEAR_ENV (line 48) are all verbatim copies of the identically-named constants already exported from extensions/anthropic/cli-shared.ts. The extension boundary guide in extensions/CLAUDE.md explicitly states: "If two bundled providers share the same replay policy shape… stop copying the logic. Extract one shared helper and migrate both call sites in the same change." Cross-extension imports are prohibited, so the correct fix is to promote these to a public openclaw/plugin-sdk subpath (e.g. openclaw/plugin-sdk/cli-backend) and import from there in both extensions. As-is, any bug fix to normalizeClaudePermissionArgs in the anthropic extension silently leaves this extension diverged.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/claude-headless/index.ts
Line: 22-118

Comment:
**Duplicated shared CLI logic violates extension boundary policy**

`normalizePermissionArgs` (lines 78–110), `MODEL_ALIASES` (lines 22–42), `SESSION_ID_FIELDS` (line 44), and `CLEAR_ENV` (line 48) are all verbatim copies of the identically-named constants already exported from `extensions/anthropic/cli-shared.ts`. The extension boundary guide in `extensions/CLAUDE.md` explicitly states: _"If two bundled providers share the same replay policy shape… stop copying the logic. Extract one shared helper and migrate both call sites in the same change."_ Cross-extension imports are prohibited, so the correct fix is to promote these to a public `openclaw/plugin-sdk` subpath (e.g. `openclaw/plugin-sdk/cli-backend`) and import from there in both extensions. As-is, any bug fix to `normalizeClaudePermissionArgs` in the anthropic extension silently leaves this extension diverged.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +39 to +42
haiku: "haiku",
"haiku-3.5": "haiku",
"claude-haiku-3-5": "haiku",
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 claude-haiku-4-5 missing from MODEL_ALIASES

The wizard modelAllowlist.allowedKeys includes "claude-headless/claude-haiku-4-5", but MODEL_ALIASES has no entry for haiku-4-5 or claude-haiku-4-5 (only haiku-3.5 / claude-haiku-3-5). After stripping the provider prefix, the alias lookup silently returns undefined and the CLI receives --model claude-haiku-4-5 verbatim instead of the haiku shorthand. Add the missing entries:

Suggested change
haiku: "haiku",
"haiku-3.5": "haiku",
"claude-haiku-3-5": "haiku",
};
haiku: "haiku",
"haiku-3.5": "haiku",
"haiku-4.5": "haiku",
"claude-haiku-3-5": "haiku",
"claude-haiku-4-5": "haiku",
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/claude-headless/index.ts
Line: 39-42

Comment:
**`claude-haiku-4-5` missing from `MODEL_ALIASES`**

The wizard `modelAllowlist.allowedKeys` includes `"claude-headless/claude-haiku-4-5"`, but `MODEL_ALIASES` has no entry for `haiku-4-5` or `claude-haiku-4-5` (only `haiku-3.5` / `claude-haiku-3-5`). After stripping the provider prefix, the alias lookup silently returns `undefined` and the CLI receives `--model claude-haiku-4-5` verbatim instead of the `haiku` shorthand. Add the missing entries:

```suggestion
  haiku: "haiku",
  "haiku-3.5": "haiku",
  "haiku-4.5": "haiku",
  "claude-haiku-3-5": "haiku",
  "claude-haiku-4-5": "haiku",
```

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a84e411b84

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

import type { ProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";

const PROVIDER_ID = "claude-headless";
const BACKEND_ID = "claude-headless";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Hook claude-headless into Claude MCP loopback path

The CLI runner only injects the OpenClaw MCP loopback server and OPENCLAW_MCP_* env vars when backendResolved.id === "claude-cli" (see src/agents/cli-runner/prepare.ts), but this plugin registers a new backend id (claude-headless). As a result, selecting this provider skips the Claude-specific loopback wiring even though bundleMcp is enabled, so headless runs miss the OpenClaw MCP tool bridge that existing Claude CLI flows depend on.

Useful? React with 👍 / 👎.

@vincentkoc

Copy link
Copy Markdown
Member

Thanks for your contribution.

The user need here is real: if someone already has a local claude auth login, OpenClaw should give them a first-class path without requiring a separate Anthropic API key.

We’re going to keep pushing that path inside the existing native Claude CLI integration rather than land a second standalone provider that duplicates the backend/auth logic.

We verified this against the Claude Code source as well. As proposed, this PR would miss hardening we now require for host-managed Claude CLI runs, including broader env scrubbing, config/plugin-root isolation, and forcing --setting-sources user so repo-local .claude project/local settings, hooks, and plugin discovery do not silently flow into non-interactive OpenClaw sessions.

We’ve also already addressed adjacent native-path issues this PR would otherwise drift from: safer auth/config patch application, correct migrated model/default replacement behavior, fallback rewrites for Claude CLI migrations, malformed permission flag fail-safe behavior, and Claude CLI image/KV stability fixes.

So we’re closing this in favor of the existing native path rather than carrying a second provider that will diverge. The useful part to keep from this PR is the UX intent: making the local Claude CLI path clearer and easier to choose. Once the native follow-up is landed, we can add the landed PR link back here as the canonical path forward.

@vincentkoc

Copy link
Copy Markdown
Member

thanks for your contribution.

follow-up landed in #61276 via 3b84884.

that native Claude CLI path now covers the user need this PR was chasing, but inside the existing hardened core flow instead of a second standalone provider: Anthropic CLI auth/default-model migration fixes, provider-owned auth patch replacement, non-interactive fallback migration, inherited Claude env/config/plugin-root scrubbing, forced --setting-sources user, malformed permission-flag fail-safe handling, and targeted regression coverage.

we still like the UX intent here: making the local Claude CLI path obvious and easy to choose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants