This file provides guidance to Claude Code when working with code in this repository.
Link CLI — lets agents get secure, one-time-use payment credentials from a Link wallet. pnpm + Turborepo monorepo:
@stripe/link-sdk(packages/sdk): Repository interfaces, API implementations, types, and local storage. Entry:src/index.ts.@stripe/link-cli(packages/cli): Commander.js + Ink/React CLI that consumes@stripe/link-sdk. Entry:src/cli.tsx.
pnpm install # install dependencies
pnpm run build # build all packages (turbo)
pnpm run dev # watch mode
pnpm run test # run all tests
pnpm run typecheck # type-check all packages
pnpm biome check . # lint + format check (CI)
pnpm run check # lint + format with auto-fixRun a single test:
cd packages/cli && pnpm vitest run src/utils/__tests__/line-item-parser.test.tsThe CLI integration tests in packages/cli/src/__tests__/cli.test.ts run against the compiled dist/cli.js. Run pnpm run build before running them if the source has changed.
Run the CLI locally:
node packages/cli/dist/cli.js <command>Defined in packages/sdk/src/resources/interfaces.ts:
IAuthResource— device auth flow (initiate, poll, refresh)ISpendRequestResource— CRUD + request-approval for spend requests
Commands in packages/cli/src/cli.tsx (incur framework). Each has two output modes:
- Interactive (default): Ink/React components from
packages/cli/src/commands/ - JSON (
--format json): JSON to stdout, errors as JSON withcodeandmessagefields with exit code 1
Commands: auth login|logout|status, spend-request create|update|retrieve|request-approval|cancel, payment-methods list, shipping-address list, mpp pay|decode, serve.
The CLI also runs as an MCP server (--mcp) and serves skill files via skills subcommand, both provided by incur.
When changing commands, flags, or schema descriptions, always update all three together: README.md, skills/create-payment-credential/SKILL.md, the schema description strings in the relevant schema.ts file, and CLAUDE.md. These can easily drift apart.
Input is passed via flags. Define options in the command's zod schema — incur registers CLI flags automatically from the schema.
auth login --client-name <name>— optional flag to identify the agent or app; shown in the user's Link app as<name> on <hostname>. Defined inloginOptionsinpackages/cli/src/commands/auth/schema.ts.auth login --interval <seconds> [--timeout <seconds>] [--max-attempts <n>]— when--intervalis provided, the command yields the verification code immediately then polls inline until authenticated or timed out. Without--interval, returns the code with a_nexthint for separate polling viaauth status.- The token endpoint echoes
scopeandauthorization_detailsback with the tokens on login/refresh. These are persisted in the credential file (part ofAuthTokens) and surfaced onauth statusin both interactive and JSON modes, only when present. - Gotcha — two parallel
AuthResourceimplementations.packages/cli/src/auth/auth-resource.tsduplicatespackages/sdk/src/resources/auth.ts(device auth flow, token parsing). The CLI uses its own viaResourceFactory.createAuthResource()(packages/cli/src/utils/resource-factory.ts) — the SDK class is not on the CLI's runtime path. Any change to token-response handling (new fields, parsing) must be applied to both, or the CLI silently drops it.
CLI command is spend-request (user-facing). Implemented in packages/cli/src/commands/spend-request/. SDK interfaces: ISpendRequestResource, CreateSpendRequestParams, UpdateSpendRequestParams. API endpoint: /spend_requests.
Key input field notes:
- CLI input uses
payment_method_id; mapped topayment_detailswhen calling the SDK contextrequires min 100 characters;amountis in cents with max 500000--testflag creates testmode credentials (real testmode SPT from test card data) instead of livemode onescreate --request-approvalandrequest-approvalboth show an approval URL in interactive mode and poll until approved/denied/expired/failed/canceled. In JSON mode (--format json), they return immediately with an_next.commandforspend-request retrieve.retrieve --interval <seconds>polls until approved/denied/expired/succeeded/failed/canceled. If--timeoutis reached or--max-attemptsis exhausted while the request is still non-terminal, it exits non-zero withPOLLING_TIMEOUT.cancel <id>cancels a spend request. Can cancel fromcreated,pending_approval, orapprovedstates. Returns the spend request withstatus: "canceled".--approval-detail— optional JSON object (MCP/agent) or JSON string (CLI) with approval details for delegated flows. Required fields:approved_at(unix timestamp int),approval_method(click|programmatic|voice),app_name,external_user_id. Optional:ip_address,user_agent,device_type(mobile|web),agent_log_id,external_user_name,external_session_id,authentication_method(biometric_face|biometric_fingerprint|passkey). Sent asapproval_detailsin the API request body.cardcredentials includebilling_address(name, line1, line2, city, state, postal_code, country) andvalid_until(ISO date string — when the card expires/stops working)--output-file <path>onretrieveorcreatewrites full card credentials to a local file (0600 permissions) and redacts card data in stdout.--forceallows overwriting an existing file.
mpp pay <url> --context <ctx> [-X <method>] [-d <body>] [-H <header>]... [--amount <cents>] [--payment-method-id <id>] [--test]— handles the full MPP flow end-to-end: probes the URL for a 402 challenge, parses thewww-authenticateheader to extract network_id and amount, creates a spend request (credential_type: shared_payment_token), gets user approval, retrieves the SPT, and pays. Amount/currency are derived from the 402 challenge;--amountoverrides.--contextis required (min 100 chars) — describe the purchase and rationale. Default payment method is used unless--payment-method-idis specified.mpp pay <url> --spend-request-id <id> [--method <method>] [--data <body>] [--header <header>]...— backward-compat mode: uses a pre-approved spend request directly, skipping creation/approval.--headeris repeatable and uses"Name: Value"format.Content-Type: application/jsonis auto-applied when--datais provided; user-provided headers take precedence.- The SPT is one-time-use — a failed payment requires running
mpp payagain (creates a new spend request). - Implemented in
packages/cli/src/commands/mpp/— pay.tsx (logic), schema.ts (input/output schema), index.tsx (incur registration).
demo [--only-card] [--only-spt]— Interactive demo of both payment flows. Always uses--testmode (no real charges). Shows a menu to choose: virtual card flow, SPT/machine payment flow, or both.--only-cardand--only-sptskip the menu. Requires a TTY (no JSON output mode).
onboard— Guided setup: authenticates (skips if already logged in), checks payment methods (prompts to add one if missing, shows picker if multiple), shows app download QR code, then runs the full demo. Requires a TTY.
serve [--port <n>] [--host <host>]— HTTP server that exposes the CLI's MCP endpoint. Implemented inpackages/cli/src/commands/serve/index.ts. The handler forwards torootCli.fetch()(incur), but is a privilege boundary:requireAuthonly proves the CLI owner is authenticated, not that the HTTP caller is authorized.
- ESM everywhere —
"type": "module"in all package.json files - Biome — 2-space indent, single quotes, organized imports
- tsup — ESM output, Node 18 target
- Vitest — test files in
__tests__/directories adjacent to source - TypeScript strict mode —
tsconfig.base.jsonat root - React 18 + Ink 5 for interactive rendering
conffor local auth token storage
| Flag | Effect |
|---|---|
--auth <path> |
Store auth credentials in a specific file instead of the default platform config location. auth login writes to this file; all other commands read from it. Parsed from process.argv and stripped before incur processes flags. |
Server-returned strings can contain ANSI escape sequences or control characters that spoof the terminal approval UI. Sanitization is handled automatically via sanitizeDeep() from packages/cli/src/utils/sanitize-text.ts:
- Commands using
useAsyncActionhook — sanitized automatically. The hook callssanitizeDeep()on all returned data before it reaches components. - Commands with manual state management (e.g.
create.tsx,retrieve.tsx,request-approval.tsx,mpp/pay.tsx) — must callsanitizeDeep()on API responses before callingsetRequest()/setState().
JSON output mode (--format json) is not affected — JSON.stringify encodes escape sequences as Unicode literals.
| Variable | Effect |
|---|---|
LINK_AUTH_FILE |
Same as --auth — override the auth credential file path (flag takes precedence) |
LINK_ACCESS_TOKEN |
Use this access token directly, bypassing auth storage |
LINK_REFRESH_TOKEN |
Refresh token to use when LINK_ACCESS_TOKEN is expired |
LINK_NO_REFRESH |
When set, never auto-refresh the access token — error instead |
LINK_API_BASE_URL |
Override API base URL |
LINK_AUTH_BASE_URL |
Override auth base URL |
LINK_HTTP_PROXY |
Route all SDK requests through an HTTP proxy (requires undici installed) |