different-ai/openwork · error · PluginArchRouteFailure

github_install_user_mismatch

github_install_user_mismatch

Error message

GitHub install state does not match the current user.

What it means

Thrown (409) in completeGithubConnectorInstall when the parsed install state's userId differs from `currentMember.userId` in the actor context. GitHub install callbacks are bound to the initiating user; completing as a different user in the same org is rejected to prevent cross-user install hijacking.

Source

Thrown at ee/apps/den-api/src/routes/org/plugin-system/store.ts:6899

    orgId: input.context.organizationContext.organization.id,
    returnPath,
    secret: env.betterAuthSecret,
    userId: input.context.organizationContext.currentMember.userId,
  })

  return {
    redirectUrl: buildGithubAppInstallUrl({ app, state }),
    state,
  }
}

export async function completeGithubConnectorInstall(input: { context: PluginArchActorContext; installationId: number; state: string }) {
  const parsedState = consumeGithubInstallState(input.state)
  if (parsedState.orgId !== input.context.organizationContext.organization.id) {
    throw new PluginArchRouteFailure(409, "github_install_org_mismatch", "GitHub install state does not match the current organization.")
  }
  if (parsedState.userId !== input.context.organizationContext.currentMember.userId) {
    throw new PluginArchRouteFailure(409, "github_install_user_mismatch", "GitHub install state does not match the current user.")
  }

  const connectorAccount = await upsertGithubConnectorAccountFromInstallation({
    context: input.context,
    installationId: input.installationId,
  })

  return {
    connectorAccount,
    // Keep install completion fast. The connected-account screen loads repositories next.
    repositories: [],
  }
}

export async function getGithubConnectorDiscovery(input: { connectorInstanceId: ConnectorInstanceId; context: PluginArchActorContext }) {
  const discovery = await resolveGithubConnectorDiscovery(input)
  return {
    autoImportNewPlugins: discovery.autoImportNewPlugins,

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Have the original initiating user complete the callback, or start a new install while logged in as the current user.
  2. Do not share or forward install completion URLs between users; each user should run their own install flow.
  3. If automating, keep the same authenticated session for both startGithubConnectorInstall and completeGithubConnectorInstall.

Example fix

// before
// state minted for user A; current session is user B
await completeGithubConnectorInstall({ installationId, state }) // 409 github_install_user_mismatch
// after
await signInAs(stateInitiatingUserId)
await completeGithubConnectorInstall({ installationId, state })
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await completeGithubConnectorInstall({ installationId, state })
} catch (e) {
  if (e instanceof PluginArchRouteFailure && e.code === 'github_install_user_mismatch') {
    await notifyOriginalUserToCompleteInstall() // or start a fresh install as current user
  } else throw e
}

Prevention

When it happens

Trigger: User A starts the install, user B (same org admin) completes the callback; the session changed users between start and callback (re-login as someone else); replaying another user's install completion URL.

Common situations: Shared admin accounts or password managers swapping sessions mid-flow; forwarding the GitHub redirect URL to a teammate to 'finish the install'; concurrent installs by two admins with mixed-up state params.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/b7570fe51db3baac. Report an issue: GitHub.