laurent22/joplin · error · Error

Encrypted items cannot be modified

Error message

Encrypted items cannot be modified

What it means

Thrown by BaseItem.save() when options.userSideValidation is true and the entity has encryption_applied set. End-to-end encryption makes item fields opaque to the client, so user-initiated edits to an encrypted item are rejected to prevent corrupting ciphertext. The message is localized via _().

Source

Thrown at packages/lib/models/BaseItem.ts:1017

		// await this.forceSync(item.id);

		return true;
	}

	public static async forceSync(itemId: string) {
		await this.db().exec('UPDATE sync_items SET force_sync = 1 WHERE item_id = ?', [itemId]);
	}

	public static async forceSyncAll() {
		await this.db().exec('UPDATE sync_items SET force_sync = 1');
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- save() accepts any BaseItemEntity subclass plus disable-readonly options; subclasses override with stricter per-entity types
	public static async save(o: any, options: SaveOptions = null) {
		if (!options) options = {};

		if (options.userSideValidation === true) {
			if (o.encryption_applied) throw new Error(_('Encrypted items cannot be modified'));
		}

		const isNew = this.isNew(o, options);

		if (needsShareReadOnlyChecks(this.modelType(), options.changeSource, this.syncShareCache, options.disableReadOnlyCheck)) {
			if (!isNew) {
				const previousItem = await this.loadItemByTypeAndId(this.modelType(), o.id, { fields: ['id', 'share_id'] });
				checkIfItemCanBeChanged(this.modelType(), options.changeSource, previousItem, this.syncShareCache);
			}

			// If the item has a parent folder (a note or a sub-folder), check
			// that we're not adding the item to a read-only folder.
			if (o.parent_id) {
				await checkIfItemCanBeAddedToFolder(
					this.modelType(),
					this.getClass('Folder'),
					options.changeSource,
					BaseItem.syncShareCache,

View on GitHub (pinned to 2654b33620)

Solutions

  1. Decrypt the item before editing — ensure the master key is provided and the item is decrypted (encryption_applied = 0) prior to save.
  2. If E2EE isn't fully set up, complete master key setup/config before editing encrypted items.
  3. Re-sync to pull the decrypted plaintext once the key is available.
  4. Audit the calling path to confirm it decrypts before opening the editor.

Example fix

// before
await Note.save(note, { userSideValidation: true });
// after - decrypt first, then save
if (note.encryption_applied) {
  await decryptionService.decryptItem(note);
}
await Note.save(note, { userSideValidation: true });
Defensive patterns

Strategy: validation

Validate before calling

if (item.encryption_applied) {
  throw new Error('Item is encrypted; decrypt it before saving user-side changes.');
}
await BaseItem.save(item, { userSideValidation: true });

Type guard

function isEncryptedItem(o: any): boolean {
  return !!o && !!o.encryption_applied;
}

Try / catch

try {
  await BaseItem.save(item, { userSideValidation: true });
} catch (e) {
  if (/Encrypted items cannot be modified/i.test(e.message)) {
    await decryptionService.decryptItem(item);
    await BaseItem.save(item, { userSideValidation: true });
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling save(item, { userSideValidation: true }) on a BaseItem whose encryption_applied is truthy — typically a note/resource that was encrypted during sync and is now being saved from a UI/editor path that sets userSideValidation.

Common situations: E2EE is enabled but the master key isn't loaded, so items stay encrypted locally while the user tries to edit them; an item was encrypted on another device and not yet decrypted here; a code path that should decrypt-before-edit skipped decryption; race where encryption was applied after the editor opened.

Related errors


AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12). Data as JSON: /api/errors/0be02c81a71a7a50. Report an issue: GitHub.