ruvnet/ruflo · error
--token-stdin: JSON is missing required field "access_token"
Error message
--token-stdin: JSON is missing required field "access_token"
What it means
Thrown by ruflo's token-stdin login path after stdin was successfully read and JSON.parse succeeded, but the resulting object has no truthy top-level `access_token` string. The CLI expects exactly one JSON object of the shape {"access_token", "refresh_token"?, "expires_in", "scope"}. It is a shape check on externally supplied credentials, not a network or auth-server failure.
Solutions
- Pipe a single flat JSON object with a non-empty top-level access_token: echo '{"access_token":"eyJ...","expires_in":3600}' | ruflo auth login --token-stdin
- If your source nests the token, unwrap it first: jq '{access_token: .data.access_token, expires_in: .expires_in}' | ruflo auth login --token-stdin
- Verify the payload before sending: cat token.json | jq -e '.access_token | type == "string" and length > 0'
- If you only hold a refresh token (no access token), use the interactive `ruflo auth login` flow instead of --token-stdin
Example fix
// before
echo '{"refresh_token":"rt_123"}' | ruflo auth login --token-stdin
// after
echo '{"access_token":"eyJhbGci...","refresh_token":"rt_123","expires_in":3600,"scope":"openid"}' | ruflo auth login --token-stdin Defensive patterns
Strategy: validation
Validate before calling
const raw = await readStdin();
let parsed: unknown;
try { parsed = JSON.parse(raw); } catch { fail('stdin is not valid JSON'); }
if (typeof (parsed as any)?.access_token !== 'string' || (parsed as any).access_token.length === 0) {
fail('payload needs a non-empty top-level "access_token" string');
}
await login({ tokens: parsed as OAuthTokenResponse, method: 'token-stdin' }); Type guard
function isTokenStdinPayload(v: unknown): v is { access_token: string; refresh_token?: string; expires_in?: number; scope?: string } {
return typeof v === 'object' && v !== null &&
typeof (v as Record<string, unknown>).access_token === 'string' &&
(v as Record<string, unknown>).access_token!.length > 0;
} Try / catch
try {
await rufloLoginFromStdin();
} catch (e) {
if (e instanceof Error && e.message.includes('missing required field "access_token"')) {
console.error('Expected: {"access_token":"...","refresh_token"?,"expires_in","scope"}');
process.exit(2);
}
throw e;
} Prevention
- Validate the token JSON shape at the source (vault/CI secret) before piping it
- Use jq -e '.access_token' as a pre-flight check in pipelines
- Keep a canonical example payload in your runbook so operators copy the right shape
When it happens
Trigger: Running `ruflo auth login --token-stdin` and piping a JSON object that lacks `access_token`: e.g. {"refresh_token":"..."} only, a wrapper like {"data":{"access_token":...}}, {"token":"..."} with the wrong key name, or access_token:"" (empty string is falsy and rejected). Any valid JSON without a non-empty top-level access_token triggers it.
Common situations: CI pipelines piping tokens from a vault/IdP whose JSON envelope nests the token one level deep; scripts written against a different CLI's token format; copy-pasting an ID-token payload with a different field name; generating the payload with jq using the wrong key.
Related errors
- --token-stdin: no input received on stdin
- login cancelled: no code was entered
- No vectors to ingest. Pass --vector "[..]" or pipe a JSON…
- not logged in for profile
- --token-stdin expects a single JSON object
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/833abddbad01133e.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/auth/client.ts:191
* `{access_token, refresh_token?, expires_in, scope}`. Wire format is not
* specified by ADR-306 — defined here as typed JSON rather than a bare
* token string, so scope/expiry are explicit rather than inferred.
*/
export async function tokenStdinLogin(input: NodeJS.ReadableStream = process.stdin): Promise<LoginResult> {
const chunks: Buffer[] = [];
for await (const chunk of input) chunks.push(chunk as Buffer);
const raw = Buffer.concat(chunks).toString('utf-8').trim();
if (!raw) throw new Error('--token-stdin: no input received on stdin');
let parsed: { access_token?: string; refresh_token?: string; expires_in?: number; scope?: string };
try {
parsed = JSON.parse(raw);
} catch {
throw new Error(
'--token-stdin expects a single JSON object: {"access_token","refresh_token"?,"expires_in","scope"}',
);
}
if (!parsed.access_token) throw new Error('--token-stdin: JSON is missing required field "access_token"');
const tokens: OAuthTokenResponse = {
access_token: parsed.access_token,
token_type: 'Bearer',
refresh_token: parsed.refresh_token,
expires_in: parsed.expires_in,
};
return { tokens, method: 'token-stdin' };
}
/**
* Refreshes an access token. Classifies failure into network-unreachable
* vs. a reachable-but-erroring server so callers can print an honest
* message instead of collapsing both into "offline" (ADR-308 failure
* policy: local ruflo functionality is never affected by auth being
* unavailable, but the diagnostic should say WHY it's unavailable).
*/
export async function refreshAccessToken(refreshTokenValue: string): Promise<OAuthTokenResponse> {View on GitHub (pinned to fa13ee4ad6)