{"record":{"id":"1ed58b205e7c2198","repo":"thedotmack/claude-mem","slug":"api-key-is-scoped-to-a-different-project","errorCode":null,"errorMessage":"API key is scoped to a different project","messagePattern":"API key is scoped to a different project","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/server/routes/v1/ServerV1PostgresRoutes.ts","lineNumber":1036,"sourceCode":"        });\n      },\n    ));\n\n    // Remote authenticated MCP endpoint. The \"secure MCP link\" a user pastes\n    // into Claude Code (or any MCP client) to recall their cloud memory:\n    //   claude mcp add --transport http claude-mem <base>/v1/mcp \\\n    //     --header \"Authorization: Bearer cm_...\"\n    // Same readAuth (memories:read) + team/project scoping + audit trail as\n    // /v1/search, so it reads identical data through identical guards. Stateless\n    // streamable-HTTP: one transport + server per request, bound to this key's team.\n    const mcpHandler = this.asyncHandler(async (req, res) => {\n      const teamId = this.requireTeamId(req, res);\n      if (!teamId) return;\n      const projectScope = req.authContext?.projectId ?? null;\n      const repo = new PostgresObservationRepository(this.options.pool);\n      const assertProjectAllowed = (projectId: string): void => {\n        if (projectScope && projectScope !== projectId) {\n          throw new Error('API key is scoped to a different project');\n        }\n      };\n      const backend: RecallBackend = {\n        search: async ({ projectId, query, limit }) => {\n          assertProjectAllowed(projectId);\n          const rows = await repo.search({ projectId, teamId, query, limit });\n          // Audit the read, same as POST /v1/search — the MCP path is no exception.\n          await this.auditWrite(req, 'observation.read', null, projectId, {\n            mode: 'search', via: 'mcp', query, limit,\n            resultCount: rows.length, observationIds: rows.map(o => o.id),\n          });\n          return rows.map(serializeObservation);\n        },\n        context: async ({ projectId, query, limit }) => {\n          assertProjectAllowed(projectId);\n          const rows = await repo.search({ projectId, teamId, query, limit });\n          await this.auditWrite(req, 'observation.read', null, projectId, {\n            mode: 'context', via: 'mcp', query, limit,","sourceCodeStart":1018,"sourceCodeEnd":1054,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/server/routes/v1/ServerV1PostgresRoutes.ts#L1018-L1054","documentation":"This error is thrown by the v1 server recall route when an API key is scoped to a specific project but the request targets a different projectId. The route captures req.authContext?.projectId as a projectScope and rejects any backend call whose projectId does not match, preventing cross-project data access with a project-scoped key.","triggerScenarios":"Calling the recall/search endpoint with an API key whose authContext.projectId is set (non-null) while the request body supplies a projectId that differs from that scope.","commonSituations":"Developers reuse an API key minted for project A to query project B's memories; copying a working request and swapping only the projectId; a shared key distributed across teams that was created with a project scope.","solutions":["Use an API key scoped to the project you are querying, or one without a project scope","Change the request's projectId to match the key's scoped project (req.authContext.projectId)","Issue a new unscoped/team-level API key via the server API key CLI if cross-project access is intended"],"exampleFix":"// before\nfetch('/api/v1/recall', { body: JSON.stringify({ projectId: 'proj-b', query: 'x' }), headers: authWithKeyScopedToProjA })\n// after\nfetch('/api/v1/recall', { body: JSON.stringify({ projectId: 'proj-a', query: 'x' }), headers: authWithKeyScopedToProjA })","handlingStrategy":"validation","validationCode":"const keyProject = req.authContext?.projectId ?? null;\nif (keyProject && keyProject !== requested.projectId) {\n  throw new Error('API key is scoped to a different project');\n}","typeGuard":"function isProjectAllowed(keyProjectId: string | null | undefined, projectId: string): boolean {\n  return !keyProjectId || keyProjectId === projectId;\n}","tryCatchPattern":"try {\n  await recallSearch(params);\n} catch (e) {\n  if (e.message === 'API key is scoped to a different project') {\n    res.status(403).json({ error: 'key project scope mismatch' });\n  } else throw e;\n}","preventionTips":["Store the key's scoped projectId alongside the key and always send the matching projectId","Prefer team-level unscoped keys for tooling that spans projects","Write an integration test per key scope asserting cross-project calls fail","Never hardcode projectId in shared request snippets; derive it from the auth context"],"tags":["auth","authorization","multi-tenancy"],"backgroundTag":"permission-denied","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}