HeyPuter/puter · error · HttpError

forbidden

forbidden

Error message

Cannot delete a protected app

What it means

Thrown by delete when the target app has `protected === true`. Protected apps are system/built-in apps that must never be removed. Returns HTTP 403 with legacyCode `forbidden`, and fires before the write-access permission check.

Source

Thrown at src/backend/drivers/apps/AppDriver.js:520

    async upsert({ uid, id, object, options } = {}) {
        const existing = uid || id ? await this.#resolve({ uid, id }) : null;
        if (existing) return this.update({ uid: existing.uid, object });
        return this.create({ object, options });
    }

    async delete({ uid, id } = {}) {
        const actor = this.#requireActor();
        this.#requireUserOrAppActor(actor);

        const app = await this.#resolve({ uid, id });
        if (!app)
            throw new HttpError(404, 'App not found', {
                legacyCode: 'not_found',
            });

        if (app.protected) {
            throw new HttpError(403, 'Cannot delete a protected app', {
                legacyCode: 'forbidden',
            });
        }

        await this.#checkWriteAccess(app, actor);
        await this.appStore.delete(app.id);

        this.#emitAppChanged({ app: null, old_app: app, action: 'deleted' });

        return { success: true, uid: app.uid };
    }

    // -- Event emission -----------------------------------------------
    //
    // Consumers (AppIconService, future cf-file-cache port, billing
    // event handlers) key off `app_uid`; the full `app` / `old_app`
    // payload lets cache invalidators compute exact origins.

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Do not delete protected/system apps; they are not removable by design.
  2. Filter your delete candidates by `app.protected === false` before calling delete.
  3. If you genuinely need it gone in a self-hosted env, clear the `protected` flag in the data store (operator action), then retry.

Example fix

// before
apps.forEach(a => driver.delete({ uid: a.uid }));

// after
apps
  .filter(a => !a.protected)
  .forEach(a => driver.delete({ uid: a.uid }));
Defensive patterns

Strategy: validation

Validate before calling

// Skip protected apps in bulk delete paths
for (const a of apps) {
  if (a.protected) continue;
  await driver.delete({ uid: a.uid });
}

Type guard

/** @param {object} app @returns {boolean} */
function isDeletable(app) {
  return Boolean(app) && app.protected !== true;
}

Try / catch

try {
  await driver.delete({ uid });
} catch (e) {
  if (e.code === 'forbidden' && app?.protected) { /* skip protected */ return; }
  throw e;
}

Prevention

When it happens

Trigger: Calling `driver.delete({ uid })` against a built-in/system app flagged `protected` (e.g. core Puter apps). The check is `if (app.protected)` immediately after the not-found check.

Common situations: Attempted cleanup script that deletes all apps indiscriminately; trying to remove a first-party app during dev/testing; confusion between a user-created app and a system app sharing a similar name.

Related errors


AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12). Data as JSON: /api/errors/83023a0d42709343. Report an issue: GitHub.