paperclipai/paperclip · error · Error
device-login credential promotion rejected: the promotion ca
Error message
device-login credential promotion rejected: the promotion carried no account home
What it means
After an accepted promotion ('promoted'/'kept'), promote() derives the account handle from result.accountId and requires result.accountHomeDir to build the CODEX_HOME_<handle> company secret. If the promotion result carries no usable accountId or no accountHomeDir, the login cannot bind agents to a durable home, so it fails closed with this error. This guards an adapter-internal invariant: a successful promotion must always report where the account home was written.
Source
Thrown at server/src/routes/agents.ts:832
logger.info({ sessionId: context.sessionId }, line);
},
}),
);
// A resolved promotion is not necessarily an accepted promotion. In
// particular, a reaper/expiry race can revoke this session's sole
// ownership between the service transition and Decision H. Fail closed:
// only a credential write or a deliberate safe keep can authenticate.
if (result.outcome !== "promoted" && result.outcome !== "kept") {
throw new Error(`device-login credential promotion rejected: ${result.outcome}`);
}
// The account's own home is durable at this point (the promotion above
// wrote it fail-loud). Name it with a company secret, so any agent can
// bind to it. Reading the secret by name first keeps a repeat login for
// the same account idempotent: `create` throws a conflict when the name
// already exists.
const handle = result.accountId ? toAccountHandle(result.accountId) : null;
if (!handle || !result.accountHomeDir) {
throw new Error(
"device-login credential promotion rejected: the promotion carried no account home",
);
}
const secretName = `CODEX_HOME_${handle}`;
const accountHomeDir = result.accountHomeDir;
const existingSecret = await secretsSvc.getByName(context.companyId, secretName);
if (existingSecret) {
// A same-name secret already exists. Confirm it still names this
// account's own home before treating a repeat login as a success:
// the name alone is not proof of a match.
//
// Run the check inside the same lock a `local_encrypted` secret
// rotate holds for its whole write, the same lock a rotate
// takes. This is an early fail-fast only: the lock is fully
// released once this call returns, well before the login
// service commits its terminal state, so queue the same check
// for `runTerminalCommit` to run again right before that
// commit, under a fresh lock acquisition it holds across theView on GitHub (pinned to 01ad858492)
Solutions
- Inspect the promotion result from promoteDeviceLoginCredential for this adapter and fix the adapter/service to always populate accountId and accountHomeDir on promoted/kept outcomes.
- Update or pin the adapter package version if a recent adapter upgrade changed the result shape.
- Check server logs for the promotion lines (logger.info with sessionId) to see which field was missing.
- If the account exists on disk but the dir wasn't reported, locate the Codex home and verify the adapter's home-derivation logic.
Example fix
// before (adapter returns partial result)
return { outcome: 'promoted', accountId: null, accountHomeDir: null };
// after
return { outcome: 'promoted', accountId: accountId, accountHomeDir: homeDir }; // always set on promoted/kept Defensive patterns
Strategy: try-catch
Type guard
function promotionCarriedAccountHome(
r: { outcome: string; accountId?: string | null; accountHomeDir?: string | null },
): r is { outcome: 'promoted' | 'kept'; accountId: string; accountHomeDir: string } {
return (r.outcome === 'promoted' || r.outcome === 'kept')
&& typeof r.accountId === 'string' && r.accountId.length > 0
&& typeof r.accountHomeDir === 'string' && r.accountHomeDir.length > 0;
} Try / catch
try {
await promoteDeviceLogin(...);
} catch (e) {
if (e instanceof Error && e.message.includes('the promotion carried no account home')) {
console.error('Adapter promotion result missing accountId/accountHomeDir — check adapter version/bug');
} else throw e;
} Prevention
- Keep the adapter package at a version known to return full promotion results
- Add an adapter-level unit test asserting accountId and accountHomeDir are set for promoted/kept outcomes
- Validate the promotion result shape immediately after promoteDeviceLoginCredential returns
- Pin adapter versions in deployment configs to avoid silent result-shape drift
When it happens
Trigger: promoteDeviceLoginCredential returns outcome 'promoted' or 'kept' but result.accountId is null/undefined (handle can't be derived) or result.accountHomeDir is empty/null — i.e. the adapter's promotion service resolved without propagating the account-home path it was supposed to write.
Common situations: Adapter (Codex) version change altering the promotion result shape; an adapter bug where the home write is skipped for a 'kept' outcome; custom/mock adapter in tests returning a partial result object.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- device-login credential promotion rejected: the existing ${s
- Adapter declares unsupported UI parser contract version — sk
- UI parser path escapes package directory — skipping
- ${prefix}: the capability must be an object.
- ${prefix}: "panelMode" must be one of ${ADAPTER_LOGIN_PANEL_
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/9a1519ff7659a7c3.
Report an issue: GitHub.