thedotmack/claude-mem · error
Forbidden
Forbidden
Error message
Project-scoped API keys cannot create projects
What it means
POST /v1/projects is guarded by writeAuth; if the authenticated API key carries a projectId scope (req.authContext.projectId is set), the route refuses with 403 Forbidden before any repository write. A project-scoped key is deliberately barred from minting new projects; only a team-level (unscoped) key may create them.
Source
Thrown at src/server/routes/v1/ServerV1Routes.ts:97
name: 'claude-mem-server',
version: BUILT_IN_VERSION,
...(this.options.runtime ? { runtime: this.options.runtime } : {}),
authMode: this.options.authMode ?? process.env.CLAUDE_MEM_AUTH_MODE ?? 'api-key',
});
});
app.get('/v1/projects', readAuth, (req, res) => {
const repo = new ProjectsRepository(this.options.getDatabase());
const projects = req.authContext?.projectId
? [repo.getById(req.authContext.projectId)].filter(project => project !== null)
: repo.list();
res.json({ projects });
this.audit(req, 'projects.list');
});
app.post('/v1/projects', writeAuth, this.handleCreate(CreateProjectSchema, (req, res, body) => {
if (req.authContext?.projectId) {
res.status(403).json({ error: 'Forbidden', message: 'Project-scoped API keys cannot create projects' });
return;
}
const project = new ProjectsRepository(this.options.getDatabase()).create(body);
this.audit(req, 'project.create', project.id);
res.status(201).json({ project });
}));
app.get('/v1/projects/:id', readAuth, (req, res) => {
const id = this.routeParam(req.params.id);
if (!this.ensureProjectAllowed(req, res, id)) return;
const project = new ProjectsRepository(this.options.getDatabase()).getById(id);
if (!project) {
res.status(404).json({ error: 'NotFound', message: 'Project not found' });
return;
}
this.audit(req, 'project.read', project.id);
res.json({ project });
});View on GitHub (pinned to e2d1df569a)
Solutions
- Use a team-scoped API key (one without a projectId) for project creation
- If you did not intend to create projects, remove that call from the client
- Confirm the key's scope first: GET /v1/projects returns exactly one project for a project-scoped key
Example fix
// before — project-scoped key
createProject(teamKeyScopedToProjectA, { name: 'new-project' }); // 403
// after — unscoped team key
createProject(teamAdminKey, { name: 'new-project' }); // 201 Defensive patterns
Strategy: validation
Validate before calling
async function isProjectScopedKey(base: string, key: string): Promise<boolean> {
const res = await fetch(`${base}/v1/projects`, { headers: { Authorization: `Bearer ${key}` } });
const { projects } = await res.json();
return projects.length === 1; // project-scoped keys see exactly their own project
}
if (await isProjectScopedKey(base, key)) throw new Error('need a team-level key to create projects'); Try / catch
const res = await fetch(`${base}/v1/projects`, { method: 'POST', headers: jsonHeaders, body: JSON.stringify({ name }) });
if (res.status === 403) {
const body = await res.json();
if (body.message?.includes('Project-scoped')) throw new Error(`Key ${keyName} is project-scoped; supply a team-level key`);
throw new Error(`forbidden: ${body.message}`);
} Prevention
- Name API keys after their scope (team-admin vs project-ingest) in your secret store
- Keep separate env vars for admin operations vs data ingestion
- Never reuse an ingestion key for provisioning scripts
When it happens
Trigger: Calling POST /v1/projects with an API key that was created with a projectId constraint. The scope check runs before the ProjectsRepository.create call, so no partial state is written.
Common situations: CI or an SDK configured with the ingestion key instead of an admin key; deployment scripts reusing a project-scoped key across environments; key rotation accidentally selecting a scoped key.
Related errors
AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20).
Data as JSON: /api/errors/9f9d50f2ca49a89a.
Report an issue: GitHub.