laurent22/joplin · error

Cannot change the note lock of a read-only note

Error message

Cannot change the note lock of a read-only note

What it means

Notes that are read-only (e.g. shared/E2EE items the current user cannot edit, enforced via itemIsReadOnlySync and the sync share cache) cannot have their lock state changed. This is a fail-closed guard for direct command invocations.

Solutions

  1. Only attempt lock changes on notes you own or that are writable (check permissions in the sharing UI).
  2. Filter read-only notes out before batch operations using the same itemIsReadOnlySync predicate.
  3. Catch the error and inform the user the note is read-only.
  4. Request edit access to the shared note.

Example fix

// before
await joplin.commands.execute('enableNoteLock', noteId);
// after
const note = await Note.load(noteId);
if (note && !itemIsReadOnlySync(ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note, Setting.value('sync.userId'), BaseItem.syncShareCache)) await joplin.commands.execute('enableNoteLock', noteId);
Defensive patterns

Strategy: validation

Validate before calling

const writable = !itemIsReadOnlySync(ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note, Setting.value('sync.userId'), BaseItem.syncShareCache);
if (writable) await joplin.commands.execute('enableNoteLock', noteId);

Type guard

const isEditableNote = (n: NoteEntity): boolean => !itemIsReadOnlySync(ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, n as ItemSlice, Setting.value('sync.userId'), BaseItem.syncShareCache);

Try / catch

try { await enableNoteLock(id); } catch (e) { if (e.message.includes('read-only')) showToast('You do not have edit access'); else throw e; }

Prevention

When it happens

Trigger: Calling enableNoteLock/disableNoteLock on a note for which itemIsReadOnlySync returns true — typically a note owned by someone else in a shared notebook or otherwise non-editable by the current sync user.

Common situations: Users attempting to lock notes in read-only shared notebooks; plugins enumerating shared notes without checking edit permissions; sync user mismatch after switching accounts.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of laurent22/joplin@981a03c5c9 (2026-09-17). Data as JSON: /api/errors/0852a47501d01e14. Report an issue: GitHub.

Appendix: source

Thrown at packages/lib/services/noteLock/setNoteLockState.ts:19

import { ModelType } from '../../BaseModel';
import BaseItem from '../../models/BaseItem';
import ItemChange from '../../models/ItemChange';
import Note from '../../models/Note';
import Setting from '../../models/Setting';
import { itemIsReadOnlySync, ItemSlice } from '../../models/utils/readOnly';
import { NoteEntity } from '../database/types';
import eventManager, { EventName } from '../../eventManager';
import isNoteLockEnabled from './isNoteLockEnabled';
import NoteLockSession from './NoteLockSession';

// The UI hides the enable/disable actions for these cases, but the commands can also be
// invoked directly (keyboard, plugins), so the transitions fail closed here too.
const checkCanChangeLockState = (note: NoteEntity, noteId: string) => {
	if (!isNoteLockEnabled()) throw new Error('Note lock is not enabled');
	if (!note) throw new Error(`No such note: ${noteId}`);
	if (note.deleted_time) throw new Error('Cannot change the note lock of a deleted note');
	if (note.is_conflict) throw new Error('Cannot change the note lock of a conflict note');
	if (itemIsReadOnlySync(ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note as ItemSlice, Setting.value('sync.userId'), BaseItem.syncShareCache)) throw new Error('Cannot change the note lock of a read-only note');
	if (!NoteLockSession.instance().isUnlocked()) throw new Error('Cannot change the note lock while the session is locked');
};

const validationFields = ['id', 'is_locked', 'deleted_time', 'is_conflict', 'share_id'];

// These only validate and emit: the note screen listens for the event and persists the
// change with a scheduled gated save.
export const enableNoteLock = async (noteId: string) => {
	const note = await Note.load(noteId, { fields: validationFields });
	checkCanChangeLockState(note, noteId);
	if (note.is_locked) throw new Error(`Note is already locked: ${noteId}`);
	eventManager.emit(EventName.NoteLockNoteStateChange, { noteId, isLocked: true });
};

export const disableNoteLock = async (noteId: string) => {
	const note = await Note.load(noteId, { fields: validationFields });
	checkCanChangeLockState(note, noteId);
	if (!note.is_locked) throw new Error(`Note is not locked: ${noteId}`);

View on GitHub (pinned to 981a03c5c9)