thedotmack/claude-mem · error
Forbidden
Forbidden
Error message
`include=payload` requires admin scope
What it means
GET /v1/jobs supports an include=payload query parameter that returns each job's full BullMQ payload, which can contain sensitive generation request data. The route refuses the call with 403 when the API key's scopes do not include '*', 'admin', or 'memories:admin'. The refusal is deliberate: rather than silently stripping the include, it keeps the attempted privilege elevation visible in the audit chain.
Source
Thrown at src/server/routes/v1/ServerV1PostgresRoutes.ts:622
// project's jobs; team-scoped key sees the team's jobs. Filters: status,
// source_type, limit, offset, since (ISO timestamp on created_at). The
// BullMQ payload column is NEVER returned by default — even with admin
// scope, the caller MUST opt in via `?include=payload`. This anti-pattern
// guard prevents accidental exfil of sensitive event payloads.
app.get('/v1/jobs', readAuth, this.asyncHandler(async (req, res) => {
const teamId = this.requireTeamId(req, res);
if (!teamId) return;
const callerProjectId = req.authContext?.projectId ?? null;
const includeRaw = typeof req.query.include === 'string' ? req.query.include : '';
const includePayload = includeRaw.split(',').map(p => p.trim()).includes('payload');
const callerScopes = req.authContext?.scopes ?? [];
const isAdmin = callerScopes.includes('*') || callerScopes.includes('admin')
|| callerScopes.includes('memories:admin');
if (includePayload && !isAdmin) {
// Anti-pattern guard: refuse the include=payload elevation without
// admin scope. Returning 403 (not silently stripping) makes the
// attempted privilege escalation visible in the audit chain.
res.status(403).json({
error: 'Forbidden',
message: '`include=payload` requires admin scope',
});
return;
}
const { status, sourceType, limit, offset, since } = parseGenericJobListingQuery(req);
let jobs: JobListRow[] = [];
let total = 0;
try {
({ jobs, total } = await this.listJobsForScope({
teamId, projectId: callerProjectId, status, sourceType, limit, offset, since,
}));
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
logger.warn('SYSTEM', 'jobs.list query failed', { requestId: req.requestId ?? null }, err);
this.handleDbError(err, res, 'jobs.list');
return;
}View on GitHub (pinned to e2d1df569a)
Solutions
- Remove 'payload' from the include query parameter and re-issue the request
- If job payload data is genuinely required, issue the API key with the 'memories:admin' (or 'admin'/'*') scope
- Verify the key's effective scopes via key introspection before switching dashboards to include=payload
Example fix
// before
const res = await fetch(`${base}/v1/jobs?include=payload`, { headers: { Authorization: `Bearer ${key}` } });
// after — only request payload when the key is known to be admin-scoped
const isAdminKey = keyScopes.some(s => s === '*' || s === 'admin' || s === 'memories:admin');
const include = isAdminKey ? 'status,payload' : 'status';
const res = await fetch(`${base}/v1/jobs?include=${include}`, { headers: { Authorization: `Bearer ${key}` } }); Defensive patterns
Strategy: validation
Validate before calling
// Only ask for payloads when the key is known to carry an admin-ish scope const ADMIN_SCOPES = new Set(['*', 'admin', 'memories:admin']); const canIncludePayload = (keyScopes: string[]) => keyScopes.some(s => ADMIN_SCOPES.has(s)); const include = canIncludePayload(keyScopes) ? 'status,payload' : 'status';
Type guard
const isAdminScope = (scopes: string[] | undefined): boolean => !!scopes && scopes.some(s => s === '*' || s === 'admin' || s === 'memories:admin');
Prevention
- Store the key's scopes alongside the key in your client config and derive include params from them
- Never hardcode include=payload in shared dashboards; make it an explicit admin-only toggle
- Treat a 403 from this route as a scope problem first — check scopes before debugging the URL
When it happens
Trigger: GET /v1/jobs?include=payload (or include=status,payload) issued with a key whose scopes are e.g. ['memories:read'] or any list without '*', 'admin', or 'memories:admin'.
Common situations: Ops dashboards or debug scripts copied from an admin-key environment onto a production read-only key; a newly minted key provisioned without admin scopes; tooling migrated from a team-admin key to a least-privilege key.
Related errors
AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20).
Data as JSON: /api/errors/ac52d041a6b60e64.
Report an issue: GitHub.