@powerhousedao/ph-clint-claude-subscription
v0.1.0-dev.1
Published
ph-clint plugin: authenticate against the Anthropic API using a Claude.ai Pro/Max subscription (OAuth) instead of an API key.
Downloads
316
Readme
@powerhousedao/ph-clint-claude-subscription
A ph-clint plugin that lets your CLI / agent authenticate against the Anthropic API with a Claude.ai Pro or Max subscription instead of (or alongside) a pay-as-you-go API key.
Under the hood, this uses the same OAuth flow as the official claude CLI:
PKCE-protected authorization, refresh-token rotation, and the
oauth-2025-04-20,claude-code-20250219 API beta header. Tokens are persisted
to the ph-clint workspace store ({workdir}/.ph/{cliName}/claude-subscription.json)
and refreshed on demand.
Important — Anthropic terms. Anthropic restricts subscription OAuth tokens to Claude-Code-shaped clients. The plugin enforces this for you: the outbound system prompt is rewritten so its first block is always the required preamble, regardless of what your agent passes. Using these tokens in unrelated products still violates the Anthropic Consumer ToS.
Install
pnpm add @powerhousedao/ph-clint-claude-subscription @ai-sdk/anthropic@^2@ai-sdk/anthropic is an optional peer dependency — only required if you use
createClaudeSubscriptionModel(). The OAuth + storage + command APIs work
without it. The plugin is tested against @ai-sdk/[email protected].
For CLI developers
1. Add the auth commands to your CLI
// cli.ts
import { defineCli } from '@powerhousedao/ph-clint';
import { createClaudeAuthCommands } from '@powerhousedao/ph-clint-claude-subscription';
import { myCommands } from './commands/index.js';
export const cli = defineCli({
name: 'my-cli',
version: '0.1.0',
description: 'My ph-clint CLI',
commands: [
...myCommands,
...createClaudeAuthCommands(), // adds claude-login / claude-logout / claude-status
],
});That's it — your users now have my-cli claude-login, claude-logout, and
claude-status available.
2. Use the subscription session in your agent factory
The example below mirrors the real-world pattern: a demo-agent escape hatch, API-key fast path, subscription fallback, and a final demo-agent fallback if nothing is configured.
// agents/agent.ts
import { Agent } from '@mastra/core/agent';
import { createWorkdirStore } from '@powerhousedao/ph-clint';
import type { AgentSetupContext, AgentProvider } from '@powerhousedao/ph-clint';
import { createMastraHelpers } from '@powerhousedao/ph-clint/mastra';
import {
createSession,
createWorkdirTokenStore,
createClaudeSubscriptionModel,
} from '@powerhousedao/ph-clint-claude-subscription';
import { CLI_NAME } from '../config.js';
import { createDemoAgent } from './demo-agent.js';
export async function createAgent(
ctx: AgentSetupContext<Config>,
): Promise<AgentProvider> {
// 1. Demo agent escape hatch (e.g. for offline development).
if (ctx.config.model === 'clint/demo-agent') return createDemoAgent();
const m = createMastraHelpers(ctx);
const store = createWorkdirStore(ctx.workdir, CLI_NAME);
const usingApiKey = Boolean(ctx.config.anthropicApiKey);
const subscription = createSession({ store: createWorkdirTokenStore(store) });
const usingSubscription =
!usingApiKey && (await subscription.isAuthenticated());
// 4. Nothing configured — fall back to demo and tell the user.
if (!usingApiKey && !usingSubscription) {
ctx.context.log?.info(
'[agent] No API key and no Claude subscription session — ' +
'run `my-cli claude-login` to authenticate with Pro/Max.',
);
return createDemoAgent();
}
// 2. API key wins when both are present.
// 3. Otherwise use the subscription session.
const model = usingSubscription
? await createClaudeSubscriptionModel({
session: subscription,
modelId: ctx.config.model, // e.g. 'anthropic/claude-sonnet-4-5'
})
: {
id: ctx.config.model as `${string}/${string}`,
apiKey: ctx.config.anthropicApiKey!,
};
const agent = new Agent({
id: 'my-agent',
name: 'My agent',
instructions: m.getAgentInstructions('my-agent'),
model,
tools: () => m.getTools(),
});
return m.wrapAgent(agent, { maxSteps: 80 });
}The session refreshes tokens automatically and the plugin enforces Anthropic's
system-prompt compliance via its fetch interceptor — your agent code doesn't
see the OAuth machinery at all.
Model ids
Pass the AI-SDK / Mastra-style provider-prefixed id:
'anthropic/claude-sonnet-4-5''anthropic/claude-haiku-4-5''anthropic/claude-opus-4-5'
The plugin strips the anthropic/ prefix before talking to the API, so the
bare id also works. Using the prefixed form keeps your config consistent
between the API-key and subscription paths.
For end users
# Start the flow — opens a URL you authorize at claude.ai.
my-cli claude-login
# Paste the code shown on the redirect page back in.
my-cli claude-login --code 'abc123#state'
# Check session status.
my-cli claude-status
# Sign out.
my-cli claude-logoutWhere tokens live. Tokens are stored in the ph-clint workspace store at
{workdir}/.ph/{cliName}/claude-subscription.json, where {workdir} is your
CLI's current workspace directory (resolved by ph-clint — usually the cwd you
ran the CLI from, not ~). If you run claude-login in one directory and
then run the agent from another, the agent won't see the tokens. Use the same
workdir, or copy the file across.
Refresh happens on the next agent call after expiry — no manual intervention needed.
API
createClaudeAuthCommands(options?)
Returns three Commands ready to drop into defineCli({ commands }):
| id | description |
| --------------- | ------------------------------------------------ |
| claude-login | Two-step OAuth login. Run without --code first.|
| claude-logout | Clear stored tokens. |
| claude-status | Show whether a session is stored + when expires. |
Options:
prefix— change the command-id prefix (default'claude').redirectUri— override the OAuth redirect URI (default = the manual-paste callback used by claude.ai).store— supply a customTokenStorefactory (default = workspace-backed).
createSession({ store })
Manages access tokens: reads from storage, refreshes when expired, dedupes
concurrent refreshes. Use session.getAuthHeaders() to mint per-request
Authorization + anthropic-beta headers.
createClaudeSubscriptionModel({ session, modelId })
Returns a LanguageModelV2 (from @ai-sdk/provider) wired to the session.
Pass directly to Mastra's Agent({ model }) or any other AI-SDK consumer.
Accepts both 'anthropic/<id>' and bare '<id>' model strings; the
anthropic/ prefix is stripped before the request is sent. The plugin's
fetch interceptor also rewrites the outbound system prompt to be
Anthropic-compliant — callers don't need to prepend anything.
Low-level OAuth helpers
createPkce(), buildAuthorizeUrl(), exchangeCode(), refreshTokens(),
parseManualCode(), isExpired(). Useful if you want to build a custom auth
flow (e.g. a TUI flow with a local HTTP listener instead of manual paste).
Troubleshooting
Cannot find package '@ai-sdk/anthropic' — declared as an optional peer.
Install it: pnpm add @ai-sdk/anthropic@^2. Mastra ships internal aliased
copies, but the unaliased name has to be in your CLI's own dependencies.
404 not_found_error: model: <id> — your model id isn't recognized.
Use one of the documented Anthropic ids (e.g. claude-sonnet-4-5); the
anthropic/-prefixed form is accepted and stripped automatically.
429 rate_limit_error with "Error" as the message and no retry-after
— this is Anthropic rejecting a non-Claude-Code-shaped request, not a real
rate limit. The plugin enforces compliance via its fetch interceptor; if
you're seeing this, you're probably bypassing createClaudeSubscriptionModel
(e.g. talking to the API directly with the session's auth headers). Send the
system prompt as a two-block array whose first block is exactly
"You are Claude Code, Anthropic's official CLI for Claude.".
Not signed in. Run claude-login... after a successful login — workdir
mismatch. Tokens are scoped to the CLI's workdir; running claude-login in
one directory and the agent in another will not share the session.
Why a separate package?
- No new framework concepts. It composes with
defineClivia plaincommandsand a plain agent-factory hook. - No core dependency. ph-clint stays auth-agnostic; this package owns one specific provider and ToS-bound behavior.
- Easy to swap. A future
ph-clint-openai-subscription(or whatever) can follow the same shape.
