paperclipai/paperclip · error · Error
device-login promotion: the account identifier cannot form a
Error message
device-login promotion: the account identifier cannot form a valid account handle
What it means
During device-login promotion, the adapter converts the Codex account identity into a sanitized account handle via toAccountHandle(). That handle names the account's home directory and the company secret, so an identity that cannot produce a safe handle is rejected before any filesystem or secret-store write occurs. This is a fail-fast invariant guarding against unsafe or malformed identifiers.
Source
Thrown at packages/adapters/codex-local/src/server/adapter-auth-promotion.ts:226
}
// 2. Validate the credential with the export rules. This rejects an empty, an
// oversized, an API-key, a non-subscription, and a malformed payload.
assertUsableSubscriptionShape(authBytes);
const accountId = readSubscriptionAccountId(authBytes);
if (!accountId) {
// The shape gate above already guarantees a subscription identity; this guard
// keeps the account_id non-null for the handle conversion without a non-null
// cast.
throw new Error("device-login promotion: the credential has no subscription identity");
}
// 2b. Convert the identity into a safe account handle. The handle names both
// this account's own home directory and its company secret, so a login
// whose identity cannot form one must fail before any write.
const accountHandle = toAccountHandle(accountId);
if (!accountHandle) {
throw new Error("device-login promotion: the account identifier cannot form a valid account handle");
}
// 3. Decision C: only a user-initiated login seeds a home.
if (!userInitiated) {
await log("[paperclip] Codex device-login promotion: skipped (an automatic background login never seeds a home).");
return { outcome: "background_skipped", accountId, accountHomeDir: null, accountHomeCreated: false };
}
// 4. Decision H: write only while the session still owns the active slot.
const soleOwner = await isSoleActiveOwner();
if (!soleOwner) {
await log("[paperclip] Codex device-login promotion: skipped (the session no longer holds the sole active claim on the slot).");
return { outcome: "not_sole_owner", accountId, accountHomeDir: null, accountHomeCreated: false };
}
// 5a. This account's own home is the durable result of a login: each account
// handle addresses exactly one home, so this write can never collide with
// a different identity, and a write failure here fails the wholeView on GitHub (pinned to 01ad858492)
Solutions
- Inspect the Codex auth source the accountId came from and confirm the account_id field is populated and well-formed.
- Re-run the Codex device login so a fresh, valid account identity is produced, then retry promotion.
- Sanitize or normalize the identifier before calling promoteDeviceLoginCredential() so toAccountHandle() succeeds.
- If the id is structurally valid but still rejected, check the toAccountHandle implementation for the exact accepted character set and align your id.
Example fix
// before
await promoteDeviceLoginCredential({ accountId: auth.lastRefreshAccountId ?? "", ... });
// after
const accountId = auth.lastRefreshAccountId;
if (!accountId || toAccountHandle(accountId) === null) {
throw new Error(`codex auth: unusable account identifier: ${JSON.stringify(auth.lastRefreshAccountId)}`);
}
await promoteDeviceLoginCredential({ accountId, ... }); Defensive patterns
Strategy: validation
Validate before calling
function canFormAccountHandle(accountId: unknown): accountId is string {
return typeof accountId === "string" && accountId.trim().length > 0 && toAccountHandle(accountId) !== null;
} Type guard
function isValidAccountId(v: unknown): v is string {
return typeof v === "string" && v.length > 0 && toAccountHandle(v) !== null;
} Try / catch
try {
await promoteDeviceLoginCredential({ accountId, ... });
} catch (err) {
if (err instanceof Error && err.message.includes("cannot form a valid account handle")) {
log("codex account id unusable; re-run device login");
return { outcome: "invalid_account_id" };
}
throw err;
} Prevention
- Always read accountId directly from the Codex auth payload field, never a derived label or email.
- Check auth.lastRefreshAccountId for non-empty before initiating promotion.
- Add a startup assertion that toAccountHandle(id) is non-null before any write path.
- Re-run Codex device login whenever auth state looks stale or incomplete.
When it happens
Trigger: Calling promoteDeviceLoginCredential() with an accountId whose shape cannot be sanitized by toAccountHandle() — e.g. an empty string, a string of only unsafe characters, or an identifier containing path separators/segments that would normalize away.
Common situations: Auth JSON from a Codex install missing or corrupting the account id field; parsing an auth payload where account_id was never set (e.g. a fresh/anonymous login); a version change upstream altering the id format; programmatic promotion with a placeholder or empty identifier.
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
- codex auth cache: account_id is not a valid account handle
- Paperclip Runner currently supports Codex only with codexPer
- Unsafe ${label}: ${result}
- persisted Codex ACPX session identity is inconsistent
- Codex working directory is required
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/0d40b8502ec3d0e5.
Report an issue: GitHub.