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
No hook matches that. Try a shorter word.
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-statusRead-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-freezeRead-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/freezeFreezes 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/extendAdds minutes to the running freeze
Who can run it: the freeze capability
axle-read-only-mode/unfreezeEnds the freeze now
Who can run it: the freeze capability
axle-read-only-mode/get-blockedRead-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-listRead-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_MODEtrue freezes the site with no end time, before the database is even needed. Remove it to unfreeze
AXLE_READ_ONLY_MODE_REASONThe reason shown while AXLE_READ_ONLY_MODE is on
AXLE_READ_ONLY_MODE_SCOPEall, 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.
| Hook | Type | Use |
|---|---|---|
axle_read_only_mode_allow_ability | filter | ( bool $allow, string $ability_name, mixed $input ). Let one ability that writes run during a freeze |
axle_read_only_mode_ability_routes | filter | ( 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_abilities | filter | ( 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
| Option | Contents |
|---|---|
axle_read_only_mode_state | The current or scheduled freeze, with its scope in area (all, content or code; missing means all) |
axle_read_only_mode_log | Blocked 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_history | The last ten freezes, with the ID of the user who started each one and each one’s scope |
axle_read_only_mode_settings | The 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.