A lock is a flag stored in post meta, beside the post status rather than instead of it. A locked post can be published, private, draft or any custom status, and front-end queries, sitemaps and feeds treat it exactly as they would an unlocked post.
_lh_post_locking_locked: present (as 1) only while the post is locked, and deleted on unlock, so finding locked posts is a query on an indexed meta key._lh_post_locking_locked_by and _lh_post_locking_locked_at: who locked the post and when (GMT), cleared on unlock.The flag is registered for the REST API so the block editor can read and save it. Only users with the lock capability can write it.
Enforcement has two layers. Both apply to anyone without the lock capability (manage_options by default).
Capabilities. A map_meta_cap filter adds do_not_allow to edit_post, edit_page, delete_post, delete_page and publish_post on a locked post. Edit and Trash links disappear, and the editor and REST API refuse changes with a proper permission error. This layer only works when the caller asks for one of those per-post capabilities.
Write path. Since 0.3.0, a backstop refuses the write itself, whatever route it takes (wp-admin, REST, MCP abilities, cron or another plugin), as long as it goes through the WordPress APIs. This catches callers that only check a type-level capability such as edit_posts.
wp_insert_post_empty_content refuses any update to a locked post: title, content, excerpt, status, parent, slug, and revision restores.pre_trash_post and pre_delete_post refuse trashing and deleting it.add_post_metadata, update_post_metadata and delete_post_metadata refuse changes to its public (non-underscore) custom fields. Underscore-prefixed meta stays writable, so internal bookkeeping such as edit locks, caches and the lock flag itself keeps working; lh_post_locking_is_frozen_meta_key changes the rule.Comment moderation is deliberately left alone. Core checks edit_comment through edit_post, so the plugin recomputes it without the lock, and editors can still approve, edit or remove comments on a locked post.
A write ability that acts on one post should check edit_post or delete_post on that post before writing, so it can give a proper “locked” error; the backstop is the safety net. When the backstop refuses an update, core reports the misleading “Content, title, and excerpt are empty.” Callers can turn that into a clear message with these static methods on LH_Post_Locking_Plugin:
last_refused_post(): the ID of the last post whose write was refused in this request, or 0.locked_error( $post_id ): a WP_Error (code lh_post_locking_locked, status 403) explaining that the post is locked.is_locked( $post ), get_lock_details( $post_id ) and blocks_write( $post_id ).run_bypassing_lock( $callback ): runs a callback with enforcement switched off, for legitimate background updaters.wp_set_object_terms(), so categories, tags and other terms on a locked post can still be changed by code that does not check edit_post first.$wpdb instead of the WordPress APIs is not seen.run_bypassing_lock() or the lh_post_locking_bypass_lock filter.Users without the lock capability see none of the editor or list-table controls.
Every post type with an admin screen and custom-fields support, except attachments. Custom-fields support is needed for the meta to reach the block editor.
A post copied with LH Copy to Draft starts unlocked, with no lock history, and no unlock event fires for it.