paperclipai/paperclip · warning

CLI auth challenge was cancelled.

Error message

CLI auth challenge was cancelled.

What it means

Startup warning at server/src/index.ts:443. The pid file check found no live postmaster, but the server could still connect with the embedded credentials (postgres://paperclip:paperclip@127.0.0.1:<configuredPort>/postgres) and `SELECT current_setting('data_directory', true)` returned a path that resolves to the same embedded dataDir. That means a PostgreSQL server is already serving this exact cluster without a usable postmaster.pid, so the server reuses it and never constructs or starts a new EmbeddedPostgres instance.

Solutions

  1. Confirm the reused server is really yours: `psql "postgres://paperclip:paperclip@127.0.0.1:54329/postgres" -c "show data_directory;"` and match it against the log message.
  2. If the running server is stale or unwanted, stop it (`lsof -ti :54329 | xargs kill`) and restart the app so it owns the postgres lifecycle and regenerates postmaster.pid.
  3. Never delete postmaster.pid of a running cluster as a 'fix'; plan one clean restart to restore normal lock-file tracking.
  4. If the pid file lives on a mount that loses it, move the embedded data dir to stable local storage via database.embeddedPostgresDataDir.

Example fix

# before: pid file removed while embedded postgres still running
rm -f ~/.paperclip/embedded-postgres/postmaster.pid
# server logs: "...reachable without a pid file; reusing existing server on configured port 54329"

# after: let the app own the lifecycle — stop the stray postgres, then start cleanly
kill $(lsof -ti :54329)
pnpm dev
Defensive patterns

Strategy: validation

Validate before calling

import { resolve } from "node:path";
import postgres from "postgres";

// Mirrors the server's probe: is the reachable server on this port OUR cluster?
export async function isExpectedEmbeddedServer(
  adminUrl: string, // e.g. postgres://paperclip:paperclip@127.0.0.1:54329/postgres
  expectedDataDir: string,
): Promise<boolean> {
  const sql = postgres(adminUrl, { connect_timeout: 2 });
  try {
    const [row] = await sql`select current_setting('data_directory', true) as d`;
    return typeof row?.d === "string" && resolve(row.d) === resolve(expectedDataDir);
  } catch {
    return false;
  } finally {
    await sql.end();
  }
}

Prevention

When it happens

Trigger: postmaster.pid was deleted, truncated, or made unreadable while the embedded postgres kept running; the data dir is on a shared/volume mount where the pid file did not survive; an operator started postgres against this data dir manually/externally so this process never tracked its pid; a crashed supervisor removed the lock on restart.

Common situations: Partial cleanup of the data dir (rm of the lock file only) while the server was up; Docker/host volume mounts that exclude or lose the pid file; hand-run `postgres -D <dataDir>` during debugging; security tooling that deletes lock files.

Related errors


AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18). Data as JSON: /api/errors/045fb9c23f7e5f9c. Report an issue: GitHub.

Appendix: source

Thrown at cli/src/client/board-auth.ts:278

            authorization: `Bearer ${challenge.boardApiToken}`,
          },
        },
      );
      setStoredBoardCredential({
        apiBase,
        token: challenge.boardApiToken,
        userId: me.userId ?? me.user?.id ?? null,
        storePath: params.storePath,
      });
      return {
        token: challenge.boardApiToken,
        approvalUrl,
        userId: me.userId ?? me.user?.id ?? null,
      };
    }

    if (status.status === "cancelled") {
      throw new Error("CLI auth challenge was cancelled.");
    }
    if (status.status === "expired") {
      throw new Error("CLI auth challenge expired before approval.");
    }

    await sleep(pollMs);
  }

  throw new Error("CLI auth challenge expired before approval.");
}

export async function revokeStoredBoardCredential(params: {
  apiBase: string;
  token: string;
}): Promise<void> {
  const apiBase = normalizeApiBase(params.apiBase);
  await requestJson<{ revoked: boolean }>(`${apiBase}/api/cli-auth/revoke-current`, {
    method: "POST",

View on GitHub (pinned to 120ae5428f)