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

  1. Use a team-scoped API key (one without a projectId) for project creation
  2. If you did not intend to create projects, remove that call from the client
  3. 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

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.