thedotmack/claude-mem · error

Forbidden

Forbidden

Error message

API key is not bound to a team

What it means

Thrown by the legacy Claude Code compat endpoint POST /api/sessions/summarize when the authenticated API key carries no team binding. The auth middleware fills req.authContext.teamId from the key record, and the compat adapter requires it because sessions are stored per team. Local-dev mode also yields teamId null unless a local-dev team is configured, so this fires there too.

Source

Thrown at src/server/compat/SessionsSummarizeAdapter.ts:59

  constructor(private readonly options: SessionsSummarizeAdapterOptions) {}

  setupRoutes(app: Application): void {
    const writeAuth = requirePostgresServerAuth(this.options.pool, {
      authMode: this.options.authMode,
      allowLocalDevBypass: this.options.allowLocalDevBypass,
      requiredScopes: ['memories:write'],
    });

    app.post('/api/sessions/summarize', writeAuth, this.asyncHandler(async (req, res) => {
      const parsed = summarizeSchema.safeParse(req.body);
      if (!parsed.success) {
        res.status(400).json({ error: 'ValidationError', issues: parsed.error.issues });
        return;
      }
      const teamId = req.authContext?.teamId ?? null;
      const projectId = req.authContext?.projectId ?? null;
      if (!teamId) {
        res.status(403).json({ error: 'Forbidden', message: 'API key is not bound to a team' });
        return;
      }
      if (!projectId) {
        res.status(400).json({
          error: 'BadRequest',
          message: 'Legacy /api/sessions/summarize requires a project-scoped API key',
        });
        return;
      }

      // Subagent contexts in legacy code emit summarize calls but the worker
      // skipped them. We preserve the legacy semantics so existing clients
      // see the same response shape.
      if (parsed.data.agentId) {
        res.json({ status: 'skipped', reason: 'subagent_context' });
        return;
      }

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Call POST /api/sessions/summarize with an API key that is bound to the team owning the sessions (create/bind one via POST /v1/keys with a team).
  2. If this is local development, configure the local-dev team id on the auth middleware so local-dev requests get a teamId.
  3. Introspect the key (key management endpoint or keys table) and confirm team_id is set before wiring the client.
  4. Migrate the client off the legacy route to the modern /v1 event/summarize surface that matches the key's scope.

Example fix

// before
await fetch(`${base}/api/sessions/summarize`, {
  method: 'POST',
  headers: { 'X-Api-Key': process.env.API_KEY!, 'Content-Type': 'application/json' },
  body: JSON.stringify({ contentSessionId: sid }),
}); // 403: key bound to no team

// after — mint/use a team-bound key first
// POST /v1/keys { teamId, scopes: ['memories:write'] } -> { key }
await fetch(`${base}/api/sessions/summarize`, {
  method: 'POST',
  headers: { 'X-Api-Key': TEAM_BOUND_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ contentSessionId: sid }),
});
Defensive patterns

Strategy: validation

Validate before calling

// Before first summarize call, confirm the key is team-bound
// (e.g. via your key registry / minting response)
function assertTeamBoundKey(keyMeta: { teamId: string | null }) {
  if (!keyMeta.teamId) {
    throw new Error('API key has no team binding; mint a team-bound key before calling /api/sessions/summarize');
  }
}

Type guard

function isMissingTeamBinding(res: Response): boolean {
  return res.status === 403;
}

interface CompatError {
  error: string;
  message: string;
}
function isForbiddenTeamBody(body: unknown): body is CompatError {
  return (
    typeof body === 'object' && body !== null &&
    'error' in body && (body as CompatError).error === 'Forbidden' &&
    'message' in body && (body as CompatError).message === 'API key is not bound to a team'
  );
}

Try / catch

try {
  const res = await fetch(url, opts);
  if (res.status === 403) {
    const body = await res.json();
    if (isForbiddenTeamBody(body)) {
      // configuration error: fix key binding, do not retry
      throw new KeyConfigError('rebind key to a team');
    }
  }
} catch (e) { if (!(e instanceof KeyConfigError)) throw e; }

Prevention

When it happens

Trigger: POST /api/sessions/summarize with a Bearer/X-Api-Key key whose record has team_id = null; running the server with local-dev auth where authContext.teamId is null; using a key minted without a team; pointing a legacy Claude Code client at a deployment whose keys predate team scoping.

Common situations: Test scripts copy an env key from another deployment that was never bound to a team; server runs in local-dev mode without localDevTeamId configured; a read-only key minted for the MCP link endpoint is reused for the legacy summarize endpoint.

Related errors


AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20). Data as JSON: /api/errors/a142eb124008268c. Report an issue: GitHub.