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
- Decrypt the item before editing — ensure the master key is provided and the item is decrypted (encryption_applied = 0) prior to save.
- If E2EE isn't fully set up, complete master key setup/config before editing encrypted items.
- Re-sync to pull the decrypted plaintext once the key is available.
- 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
- Ensure the E2EE master key is loaded before editing.
- Decrypt items before opening them in the editor.
- Don't call save with userSideValidation on ciphertext; decrypt first.
- Re-sync to obtain plaintext after enabling/adding the master key.
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
- Locked notes cannot be opened in an external editor
- Cannot change encrypted item
- Authentication failed! ${error}
- Validation error: ${k} cannot contain a null byte
- No item with ID ${itemId}
AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12).
Data as JSON: /api/errors/0be02c81a71a7a50.
Report an issue: GitHub.