Skip to content

feat(auth): opt-in FABRO_CLI_TRUST_HOST_AUTH so claude CLI can use a host-side Max subscription#259

Open
PeterBell wants to merge 2 commits into
fabro-sh:mainfrom
gathercommunity:sloane/cli-uses-oauth-anthropic
Open

feat(auth): opt-in FABRO_CLI_TRUST_HOST_AUTH so claude CLI can use a host-side Max subscription#259
PeterBell wants to merge 2 commits into
fabro-sh:mainfrom
gathercommunity:sloane/cli-uses-oauth-anthropic

Conversation

@PeterBell
Copy link
Copy Markdown

Problem

For operators on a Claude Max subscription who want to use the cli backend with provider="anthropic", the current behavior silently bills the registered Anthropic API key per token rather than consuming Max quota. The flow:

  1. Operator runs fabro provider login --provider anthropic and registers an API key.
  2. Operator authors a workflow with backend="cli" (the documented path to use the inner claude CLI's own auth).
  3. On run, Fabro's credential resolver returns CliCredential { env_vars: { "ANTHROPIC_API_KEY": ... } }. launch_env.rs injects that into the spawned subprocess. claude CLI sees ANTHROPIC_API_KEY in its env and prefers it over any host-side OAuth credentials it already has.
  4. The run consumes API tokens, billed against the registered key, even though the operator has Max and wants to use it.

Verified on 0.232.0-nightly.0 against the latest claude CLI (2.1.77):

  • claude CLI's init payload in agent.cli.completed reports apiKeySource: "ANTHROPIC_API_KEY".
  • Anthropic Console balance shows real per-token spend (confirmed at total_cost_usd: 0.063 on a trivial single-step lookup).

This is a blocker for the subscription-only deployment shape: any operator who wants Fabro orchestration with Max-covered execution cannot get there with current Fabro.

Short-term fix in this PR

Add an opt-in env var, FABRO_CLI_TRUST_HOST_AUTH. Three files, +41 lines:

  • lib/crates/fabro-static/src/env_vars.rs: declare the constant.
  • lib/crates/fabro-server/src/spawn_env.rs: add it to WORKER_ENV_ALLOWLIST so it reaches the worker that runs the resolver.
  • lib/crates/fabro-auth/src/resolve.rs: when the flag is truthy AND the credential is (Provider::Anthropic, AuthDetails::ApiKey, CliAgentKind::Claude), to_cli_credential returns CliCredential { env_vars: empty, login_command: None }.

Behavior unchanged for: any unset / falsy flag value, any provider other than Anthropic, any credential variant other than ApiKey, any CLI agent other than Claude. Existing API-key users see no change.

When the flag is set, claude CLI starts with no ANTHROPIC_API_KEY in its environment and falls back to whatever credentials it finds on the host (typically a Max subscription OAuth session at ~/.claude/). End-to-end verification on a Max subscription:

  • apiKeySource in the agent.cli init payload flips from "ANTHROPIC_API_KEY" to "none".
  • Anthropic Console-side token counts do not increase across patched runs.
  • A registered Anthropic API key is still required to satisfy the resolver gate; the value is never propagated.

The reasoning behind the env-var-and-allowlist approach (rather than a settings.toml field): smallest possible diff, easy to reason about, easy to revert. Operator's settings.toml stays untouched. Happy to refactor to a [providers.anthropic] cli_skip_env_credentials = true field in settings.toml if you'd prefer that surface; the resolver-side logic is the same either way.

A more robust solution we'd love to work on

This PR is the smallest correct change to unblock Max-subscription operators today. We'd be happy to follow up with a proper Anthropic OAuth credential path that mirrors the existing CodexOAuth shape for OpenAI:

  • New AuthDetails::AnthropicOAuth { tokens, account_id, ... } variant alongside AuthDetails::ApiKey.
  • fabro provider login --provider anthropic defaults to a browser OAuth PKCE flow (with --api-key fallback for headless setups), tied to the operator's Max account.
  • Tokens stored in Fabro's vault; refresh handled by the existing refresh_oauth_credential infrastructure that already covers Codex.
  • to_cli_credential for (Anthropic, AnthropicOAuth, Claude) either (a) returns an empty env_vars with a login_command that runs claude /login (or equivalent) on host-side, or (b) injects the OAuth access_token directly. Whichever matches Anthropic's actual CLI semantics; we'd dig into claude CLI's auth contract in the implementation.

The two approaches coexist cleanly. The env-var flag from this PR becomes "I'll manage claude's auth on the host myself"; the OAuth path becomes "Fabro manages it for me." Different deployment shapes call for different modes. No migration cost for operators on either path.

Would you be open to a follow-on PR for the OAuth variant? We have bandwidth to start on it today if there's interest. Happy to align on the credential-variant shape and the provider login UX before we write code, or to send a draft for review. If you'd prefer to leave the OAuth path on the roadmap and merge just this minimal env-var fix for now, that's a clean stopping point too.

Notes

  • This PR is from gathercommunity/fabro, a fork in the gather.community org. Filing on behalf of a small team that's building an agentic orchestration layer on Fabro for our own workflows.
  • We've been running the patched binary for a few hours of testing against a real Max-subscription workflow; no surprises beyond what's documented here.
  • The 3 test failures on cargo test --release -p fabro-server --lib in our environment (install::tests::write_artifact_store_metadata_creates_marker_in_overridden_storage_root, serve::tests::build_object_store_from_settings_rejects_partial_static_credentials, serve::tests::build_slatedb_store_uses_configured_local_root) reproduce on a clean main checkout in the same environment, so they are pre-existing environment-dependent issues unrelated to this change. Both spawn_env tests pass.

Thanks for Fabro. The DOT-graph model + the CLI-backend escape hatch is exactly the shape we needed.

…n for claude CLI

Today, when an Anthropic API key is registered via `fabro provider login`,
Fabro injects ANTHROPIC_API_KEY into the claude CLI subprocess environment.
The claude CLI then prefers that key over its own host-side credentials.
For operators on a Claude Max subscription who want CLI-backend runs to
consume Max quota rather than per-token API spend, this silently routes
billing through the API key.

This commit adds an opt-in env var, FABRO_CLI_TRUST_HOST_AUTH. When set to
a truthy value (any non-empty value except "0" or "false"):

  - resolve::to_cli_credential, when called for (Anthropic, ApiKey, Claude),
    returns CliCredential { env_vars: empty, login_command: None }.
  - launch_env.rs therefore does not insert ANTHROPIC_API_KEY into the
    spawned claude subprocess env.
  - claude CLI falls back to its own credentials on the host (e.g. a
    Max subscription OAuth session at ~/.claude/), reporting
    apiKeySource: "none" in its init payload.
  - A registered credential is still required to satisfy the resolver
    gate; the key value itself is never propagated to the subprocess.

The flag must reach the worker subprocess that runs the credential
resolver. spawn_env.rs's WORKER_ENV_ALLOWLIST is extended to include
FABRO_CLI_TRUST_HOST_AUTH, so a server with the flag set propagates it
to workers it spawns.

Behavior unchanged when the flag is unset, when the provider is not
Anthropic, when the credential is not an ApiKey, or when the CLI is
not Claude. All other backends and providers continue to receive the
existing env injection.

Tested end-to-end against a Claude Max subscription: with the flag
set, a workflow run reports apiKeySource: "none" in the claude CLI's
init payload and the Anthropic console-side token count does not
increase between the credential-gate-only run and a real run.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@brynary
Copy link
Copy Markdown
Member

brynary commented May 13, 2026

Thanks @PeterBell! I'm going to dig into this while on the flight I'm on.

Longer term, I'm hoping to deprecate and eventually replace cli mode with acp. Could you give acp a try because if we can make ACP mode use your subscription properly that's a better long term path?

@PeterBell
Copy link
Copy Markdown
Author

Perfect - once I've spiked Codex in CLI mode, I'll run a new spike for both Claude and Codex using ACP

@brynary
Copy link
Copy Markdown
Member

brynary commented May 13, 2026

Great. Also if you run into issues, I would use fabro secret rm to delete the API key secrets so they are inaccessible to Fabro and it doesn't inadvertently use them.

Future setup improvement will be making them optional.

@PeterBell
Copy link
Copy Markdown
Author

Thanks - will do. Although honestly I just rotate them to be safe!

The original opt-in covered only (Anthropic, ApiKey, Claude). Extending
to (OpenAI, ApiKey, Codex) and (OpenAI, CodexOAuth, Codex) closes the
auth-clobber path the CLI-backend Codex spike hit in Phase 1:
`resolve_agent_launch_env` was running `codex login --with-api-key`
against the vault credential, which overwrites ~/.codex/auth.json with
an OAuth access_token that lacks `api.responses.write` scope. Direct
codex CLI invocations 401 after that, regardless of Fabro.

With the flag set, the new arms return empty env_vars and no
login_command for both OpenAI+Codex variants. Codex (and codex-acp under
the ACP backend) falls back to ~/.codex/auth.json, which is the user's
ChatGPT Pro session.

Renamed claude_cli_trust_host_auth → cli_trust_host_auth to reflect
that it now covers two (provider, agent) pairs. Env var name unchanged.

Validated end-to-end on ACP backend with smoke-acp-claude.fabro and
smoke-acp-codex.fabro at gathercommunity/cura-fabro. Both runs succeed,
both return cura data, ~/.codex/auth.json shape (auth_mode="chatgpt")
preserved post-run.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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