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
- 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).
- If this is local development, configure the local-dev team id on the auth middleware so local-dev requests get a teamId.
- Introspect the key (key management endpoint or keys table) and confirm team_id is set before wiring the client.
- 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
- Store the key's team binding alongside the key in client config and assert it at startup.
- Run a preflight health check request after configuring a new key.
- Never reuse MCP link read-only keys for the legacy summarize route.
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.