Axle Read-Only Mode developer reference

Hooks

Every default, limit and message has a filter, and every change fires an action.

axle_read_only_mode_default_durationFilter
( int $minutes )

Length when none is given: the admin bar quick freeze, wp axle-read-only-mode on and the PHP API. Also preselected on the screen. Default 60, 0 for no end

axle_read_only_mode_durationsFilter
( int[] $minutes )

Lengths offered on Tools > Read-Only Mode. Default 30, 60, 120, 240, 480 and 0

axle_read_only_mode_scopesFilter
( array $scopes )

The scopes a freeze can have. Each: label, status, description, covers (the areas it freezes, content and code). Keep all first

axle_read_only_mode_default_scopeFilter
( string $scope )

Scope when none is chosen: the admin bar quick freeze, WP-CLI, abilities, the PHP API. Also preselected on the screen. Default all

axle_read_only_mode_scopeFilter
( string $scope, array $state )

The scope of the freeze in force

axle_read_only_mode_request_areaFilter
( string $area, string $kind, string $subject, array $context )

Sort one request into content or code. $kind is admin, rest, ajax, ability, cron or front-end

axle_read_only_mode_code_screensFilter
( string[] $screens )

Admin files that change settings, plugins, themes, updates or accounts

axle_read_only_mode_code_routesFilter
( string[] $routes )

REST route prefixes that change settings, plugins, themes, templates or accounts

axle_read_only_mode_code_ajax_actionsFilter
( string[] $actions )

Ajax actions that install, change or update code and settings

axle_read_only_mode_code_ability_wordsFilter
( string[] $words )

Words in an ability name that mark it as settings or code

axle_read_only_mode_code_cron_hooksFilter
( string[] $hooks )

Scheduled events that check for updates or run site checks. Every other event counts as content

axle_read_only_mode_code_xmlrpc_methodsFilter
( string[] $methods )

XML-RPC methods a settings and code freeze removes

axle_read_only_mode_checksFilter
( array $checks, array $context )

The checks on the Before you freeze card: ID to callable. Add yours, or unset a built-in one: editing, scheduled_posts, orders, cron. A callable gets the context (start, end, until, minutes, scope, cron, visitors, now, user_id) and returns an item (tone, word, title, detail, rows, count) or null

axle_read_only_mode_check_itemsFilter
( array $items, array $context )

The items after every check ran

axle_read_only_mode_check_editorsFilter
( array $editors )

The people the card says are editing. Each: post_id, title, user_id, name, time

axle_read_only_mode_check_open_windowFilter
( int $seconds )

How far ahead the checks look when a freeze has no end time. Default one day

axle_read_only_mode_check_order_ageFilter
( int $seconds )

How recent an unpaid WooCommerce order must be to count as waiting for payment. Default two hours

axle_read_only_mode_extend_minutesFilter
( int[] $minutes )

The extend buttons. The first one is also the admin bar’s "Add 30 minutes". Default 30 and 60

axle_read_only_mode_is_frozenFilter
( bool $frozen, array $state )

Freeze or unfreeze from outside, such as one switch for every site. $frozen includes a network freeze. Add it from a must-use plugin. AXLE_READ_ONLY_MODE always wins

axle_read_only_mode_messageFilter
( string $message, array $state )

What logged-in users see

axle_read_only_mode_visitor_messageFilter
( string $message, array $state )

What visitors get when they send a form

axle_read_only_mode_user_can_bypassFilter
( bool $exempt, int $user_id )

Let a user keep working during a freeze

axle_read_only_mode_allowed_ajax_actionsFilter
( string[] $actions )

Admin-ajax actions that keep working: the read-only ones plus the ones on the allow-list. Add one, or take one off

axle_read_only_mode_allow_requestFilter
( bool $allow, string $where, string $source )

Let one wp-admin or front-end request through, such as a single sign-on callback. $allow is already true when the allow-list lets the request through, so code can return false to stop it

axle_read_only_mode_allow_rest_requestFilter
( bool $allow, WP_REST_Request $request )

Let one REST request through. Like the one above, it runs after the allow-list

axle_read_only_mode_read_only_routesFilter
( string[] $routes )

REST route prefixes that only read even when called with POST. Default /wp/v2/block-renderer/

axle_read_only_mode_writing_screensFilter
( string[] $screens )

Admin screens blocked even when only opened. Default post-new.php and upgrade.php

axle_read_only_mode_logged_visitorsFilter
( int $limit )

Anonymous blocked attempts logged per freeze. Default 10, 0 for none. Requests let through have their own limit

axle_read_only_mode_log_sizeFilter
( int $size )

Entries the log keeps, blocked and allowed. Default 100

axle_read_only_mode_history_sizeFilter
( int $size )

Past freezes the history keeps. Default 10

axle_read_only_mode_capabilityFilter
( string $capability )

Who may freeze and unfreeze. Default manage_options

axle_read_only_mode_logged_allowedFilter
( int $limit )

Requests let through that the log keeps per freeze. Default 25, 0 for none

axle_read_only_mode_allow_limitFilter
( int $limit )

Entries each part of the allow-list holds. Default 50

axle_read_only_mode_validate_allow_entryFilter
( string $error, string $type, string|int $value )

Runs after the built-in checks of an allow-list entry. Return a message to refuse it. $type is rest, paths, ajax or users

axle_read_only_mode_allow_suggestionsFilter
( array $suggestions )

The "Add" buttons on the Settings tab. Each: plugin, type, value, why. They are only suggestions: nothing is added without a click

axle_read_only_mode_notify_eventsFilter
( string[] $events, bool $network )

The events that send an email: started, ended, expired, extended, scheduled_start

axle_read_only_mode_notify_recipientsFilter
( string[] $recipients, string $event, array $freeze, bool $network )

Who gets the email. Default the addresses in the settings, or the admin email

axle_read_only_mode_notify_subjectFilter
( string $subject, string $event, array $freeze, bool $network )

The email subject

axle_read_only_mode_notify_bodyFilter
( string $body, string $event, array $freeze, bool $network, array $context )

The plain-text body. $context has minutes (extended), ended_by and blocked (ended)

axle_read_only_mode_notify_intervalFilter
( int $seconds, string $event )

Seconds before the same event can send another email. Default 60, never less

axle_read_only_mode_health_overdue_minutesFilter
( int $minutes )

Minutes a freeze can run past its end time before Site Health calls it critical. Default 30

axle_read_only_mode_health_open_ended_hoursFilter
( int $hours )

Hours a freeze with no end time can last before Site Health calls it critical. Default 6

axle_read_only_mode_health_resultFilter
( array $result, string $code )

The Site Health result. $code is off, frozen, overdue, open_ended or constant

axle_read_only_mode_health_infoFilter
( array $fields )

The fields of the Site Health > Info section

axle_read_only_mode_network_capabilityFilter
( string $capability )

Who may freeze and unfreeze the network. Default manage_network_options

axle_read_only_mode_network_log_sitesFilter
( int $limit )

Sites the network log reads, most recently active first. Default 100

axle_read_only_mode_network_site_picker_limitFilter
( int $limit )

Sites the "chosen sites" list shows. Default 200

axle_read_only_mode_startedAction
( array $state )

A freeze started or was scheduled. $state['area'] is its scope: all, content or code

axle_read_only_mode_endedAction
( array $state, string $ended_by )

A freeze ended: user, cli, ability or expired

axle_read_only_mode_extendedAction
( array $state, int $minutes )

A freeze got longer

axle_read_only_mode_blockedAction
( string $where, string $source, int $user_id, string $area )

Every blocked request, including the ones the log leaves out. $area is content or code. For an outside log or an alert channel

axle_read_only_mode_checkedAction
( array $result )

The Before you freeze checks ran, from the screen, WP-CLI or an ability. $result has items, editing, attention and context

axle_read_only_mode_scheduled_startedAction
( array $state )

A freeze that was set for later has started. Fires on the first request after its start time

axle_read_only_mode_allowedAction
( string $where, string $source, int $user_id, string $via, string $area )

Every request the allow-list (settings) or a filter (code) let through a freeze, including the ones the log leaves out

axle_read_only_mode_allow_list_savedAction
( array $allow, bool $network )

The allow-list was saved, from the screen, WP-CLI or a suggestion button

axle_read_only_mode_notify_settings_savedAction
( array $notify, bool $network )

The email settings were saved

axle_read_only_mode_notifiedAction
( string $event, string[] $recipients, string $subject, bool $sent, array $freeze, bool $network )

An email was handed to wp_mail()

axle_read_only_mode_network_startedAction
( array $state )

A network freeze started or was scheduled

axle_read_only_mode_network_scheduled_startedAction
( array $state )

A network freeze that was set for later has started

axle_read_only_mode_network_extendedAction
( array $state, int $minutes )

A network freeze got longer

axle_read_only_mode_network_endedAction
( array $state, string $ended_by )

A network freeze ended

WP-CLI

wp axle-read-only-mode check [--minutes=<n>] [--start=<datetime>] [--scope=<all|content|code>] [--no-cron] [--no-visitors] [--format=table|json]
wp axle-read-only-mode on [--minutes=<n>] [--reason=<text>] [--visitor-message=<text>] [--scope=<all|content|code>] [--no-visitors] [--no-cron] [--network] [--sites=<ids>]
wp axle-read-only-mode off [--network]
wp axle-read-only-mode extend <minutes> [--network]
wp axle-read-only-mode status [--network]
wp axle-read-only-mode log [--network] [--format=table|csv|json|count]
wp axle-read-only-mode allow <rest|path|ajax|user> <value> [--network]
wp axle-read-only-mode disallow <rest|path|ajax|user> <value> [--network]
wp axle-read-only-mode allowed [--network] [--format=table|csv|json|count]
wp axle-read-only-mode notify [--enable|--disable] [--to=<emails>] [--events=<list>] [--network]
wp axle-read-only-mode health [--format=table|json]

Abilities

What AI agents can do through the WordPress Abilities API and MCP, with the same permissions as the screen.

axle-read-only-mode/get-status

Read-only. Whether the site is frozen, until when and why, what holds it (source: site, network or constant), and its scope

Who can run it: edit_posts

axle-read-only-mode/check-freeze

Read-only. What a freeze would interrupt right now: people editing, scheduled posts and tasks due, orders waiting for payment. Input: minutes, start, scope, cron, visitors. Run it before freeze

Who can run it: the freeze capability

axle-read-only-mode/freeze

Freezes the site. Input: minutes, reason, visitor_message, visitors, cron, scope (all, content or code)

Who can run it: the freeze capability

axle-read-only-mode/extend

Adds minutes to the running freeze

Who can run it: the freeze capability

axle-read-only-mode/unfreeze

Ends the freeze now

Who can run it: the freeze capability

axle-read-only-mode/get-blocked

Read-only. What the freeze blocked, and the requests the allow-list let through (result), newest first. Input: limit

Who can run it: the freeze capability

axle-read-only-mode/get-allow-list

Read-only. The allow-list: REST routes, paths, ajax actions and user IDs

Who can run it: the freeze capability

Constants

Set these in wp-config.php.

AXLE_READ_ONLY_MODE

true freezes the site with no end time, before the database is even needed. Remove it to unfreeze

AXLE_READ_ONLY_MODE_REASON

The reason shown while AXLE_READ_ONLY_MODE is on

AXLE_READ_ONLY_MODE_SCOPE

all, content or code: the scope of a freeze that AXLE_READ_ONLY_MODE forces. Default all

Built from REFERENCE.md for version 3.1.0.

More on hooks

// Let the person running the migration keep working.
add_filter( 'axle_read_only_mode_user_can_bypass', function ( $exempt, $user_id ) {
	return $exempt || user_can( $user_id, 'manage_network' );
}, 10, 2 );

// Deploys here take longer: default to two hours, and offer 15 minutes too.
add_filter( 'axle_read_only_mode_default_duration', fn() => 120 );
add_filter( 'axle_read_only_mode_durations', fn( $minutes ) => array_merge( array( 15 ), $minutes ) );

// Tell the team channel about every blocked save.
add_action( 'axle_read_only_mode_blocked', function ( $where, $source, $user_id ) {
	if ( $user_id ) {
		my_team_alert( sprintf( 'Freeze blocked %s for user %d', $where, $user_id ) );
	}
}, 10, 3 );

// Single sign-on callbacks cannot wait for the freeze to end.
add_filter( 'axle_read_only_mode_allow_request', function ( $allow, $where ) {
	return $allow || 0 === strpos( $where, 'POST /sso/callback' );
}, 10, 2 );

Payment providers such as Stripe and PayPal retry webhooks that get a 503, so orders paid during a freeze arrive once it ends. Let a webhook through only if its provider does not retry.

More on abilities

On WordPress 6.9 and later, Axle Read-Only Mode registers abilities: named actions with input and output schemas and a permission check. AI agents find and run them through the MCP adapter, and anything else can use the REST API under /wp-abilities/v1/. Each one is marked public, which the MCP adapter needs before it lists an ability.

During a freeze, every ability that is not read-only is refused, from any plugin and from anywhere, WP-CLI included, because agents also call abilities over WP-CLI. Axle Read-Only Mode’s own abilities keep working. The refusal shows up in the blocked list as ability <name>. This needs WordPress 7.1, which added the wp_pre_execute_ability filter. On 6.9 and 7.0 the REST guard still blocks other abilities that run over HTTP.

HookTypeUse
axle_read_only_mode_allow_abilityfilter( bool $allow, string $ability_name, mixed $input ). Let one ability that writes run during a freeze
axle_read_only_mode_ability_routesfilter( string[] $routes ). REST routes that run abilities, checked per ability on 7.1 and later. Default /wp-abilities/v1/ and /mcp/
axle_read_only_mode_dispatch_abilitiesfilter( string[] $abilities ). Abilities that only run other abilities, such as the MCP adapter’s mcp-adapter/execute-ability. They get through a freeze, and the ability they run is checked instead

There is no ability that changes the allow-list. It loosens the freeze, so only people change it.

More on WP-CLI

--minutes=0 freezes until wp axle-read-only-mode off. The default is 60.

check shows what a freeze would interrupt and always exits 0, so a script can read it and decide. --scope=content freezes only content, --scope=code only settings and code; the default is all.

health prints the Site Health result and exits with status 2 when it is critical, so a monitor can alert on it.

On multisite, a command acts on the site WP-CLI loaded: the main site, or the one given with the global --url. Add --network to freeze, end, extend, list or configure the whole network instead, and --sites=2,5 with on --network to freeze only those sites.

wp axle-read-only-mode check --minutes=30                           # look first
wp axle-read-only-mode on --scope=content --minutes=120             # content migration, deploys still work
wp axle-read-only-mode on --scope=code --minutes=30                 # code deploy window, editors keep writing
wp axle-read-only-mode on --url=shop.example.com --minutes=30      # one site
wp axle-read-only-mode on --network --minutes=30                   # every site
wp axle-read-only-mode on --network --sites=2,5 --minutes=30       # chosen sites
wp axle-read-only-mode off --network

PHP API

$freezer = AryanX\Axle\ReadOnlyMode\Plugin::instance()->freezer();

$freezer->freeze( array( 'minutes' => 30, 'reason' => 'Moving servers' ) );
$freezer->extend( 15 );
$freezer->unfreeze();

Data

OptionContents
axle_read_only_mode_stateThe current or scheduled freeze, with its scope in area (all, content or code; missing means all)
axle_read_only_mode_logBlocked attempts, and the first few requests the allow-list let through, during the current or last freeze (up to 100): time, user ID, the address without its query string, where it came from, whether it was allowed, and area (what it would have changed: content or code)
axle_read_only_mode_historyThe last ten freezes, with the ID of the user who started each one and each one’s scope
axle_read_only_mode_settingsThe allow-list and the email settings
axle_read_only_mode_expired_<start>, axle_read_only_mode_started_<start>Mark a freeze as expired, or a scheduled one as started, so busy sites record each once
axle_read_only_mode_mail_<event> (transient)One minute, so an event sends at most one email a minute

On a multisite network the network keeps its own, as network (site) options: axle_read_only_mode_network_state, axle_read_only_mode_network_history, axle_read_only_mode_network_settings, and the same markers with network_ in the name.

Every logged-in attempt is logged. Anonymous attempts stop being written after the first 10 per freeze, so bots posting forms cannot turn every blocked request into a database write. Uninstalling removes all of it on every site of a multisite network, and the network options.