How it works

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.

Storage

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

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.

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.

For callers

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:

Known limits

Interface

Users without the lock capability see none of the editor or list-table controls.

Lockable post types

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.

Copies

A post copied with LH Copy to Draft starts unlocked, with no lock history, and no unlock event fires for it.