Skip to content

Hooks: Moderation, Trust, and Authentication

The action and filter seams for content moderation (reports, removals, strikes, suspensions, shadow bans, appeals), the automated content-safeguard pipeline, and the authentication surface (two-factor, registration spam control, email verification, social login). This page is for developers building moderation tooling, anti-spam integrations, trust-and-safety dashboards, or custom sign-in providers. Every hook below is fired or applied by BuddyNext Free, so it is available without Pro - the same seams are where BuddyNext Pro’s Moderation Rules engine plugs in.

The moderation queue whose report, strike, suspension, and content-safeguard hooks are documented on this page

  • Actions fire after the state change commits. A removal, strike, or suspension action runs after the database write succeeds. Listeners that need the full row should re-fetch by ID through the relevant service (for example buddynext_service( 'moderation' )), not reconstruct it from the passed scalars.
  • The safeguard pipeline is a single filter. Every automated content rule - banned words, blocked links, rate limits, duplicate holds, the new-member gate, and Pro’s keyword/ML blocklists - resolves through one filter, buddynext_safeguard_check. It runs on create and on edit. Return a WP_Error to block, or pass the value through to allow.
  • Registration filters are gates or scores. buddynext_spam_protection_enabled and buddynext_registration_challenge_enabled return booleans; buddynext_registration_spam_score returns an integer score; the domain-list filters return arrays. BuddyNext never blocks a sign-up by calling addon code directly - it reads these values.
  • Two-factor is advisory, never a hard block. buddynext_2fa_required_roles surfaces a UI hint for the listed roles; BuddyNext does not refuse sign-in for a member who has not enabled 2FA.
  • The code is the contract. Free ships no docs/specs/HOOKS.md; an earlier revision of this page cited one as a “locked” source of truth, and it does not exist. Where a signature below differs from the live do_action() / apply_filters() call site, the live code wins.

The safeguard pipeline: buddynext_safeguard_check

Section titled “The safeguard pipeline: buddynext_safeguard_check”

This is the single most important seam on this page. Every post (and every edit) runs through SafeguardService, which executes the built-in automated checks in order and then applies buddynext_safeguard_check as the final gate. This is the extension point BuddyNext Pro’s Moderation Rules engine hooks to add keyword blocklists and ML scoring, and the same seam your own anti-spam logic plugs into.

The built-in checks that run before the filter, in order (SafeguardService::check()):

  1. Blocked IP (option buddynext_blocked_ips)
  2. Banned words, site-wide (option buddynext_banned_words) and per-space (the space’s own banned_words field, read through buddynext_get_space_field())
  3. Blocked link domains (option buddynext_blocked_domains)
  4. Post rate limit per user (option buddynext_post_rate_limit)
  5. Banned hashtags (resolved through HashtagService::first_banned_in_text())

The filter runs here. Two gates run after it, and both return a hold-for-review verdict rather than a rejection:

  1. Duplicate-content hold (option buddynext_duplicate_post_window)
  2. New-member review gate (option buddynext_new_member_post_threshold)

The filter is deliberately placed before 6 and 7: a hold must never outrank a hard block, so a Pro severity=block rule wins over a new-member hold.

On a create, all of the above run. On an edit, SafeguardService::check_content() re-runs only the content-based checks (banned words, blocked domains, banned hashtags), because the rate-limit, duplicate, and new-member gates are create-time concerns. Either way, buddynext_safeguard_check runs, so your filter applies to both create and edit.

Because it runs on both, the filter is passed a final $context argument - 'create' or 'edit' - so your callback can draw the same line the built-ins do. If your rule is a “how many/how often” rule, check it. Anything counting an author’s recent activity (rate limits, flood control, cooldowns) must skip 'edit': an edit is not a new post, and re-asking the question there means an author who has hit your cap can no longer edit the posts they already published. Content rules (banned words, links, ML scoring) should keep running on edits, or editing becomes a way to smuggle content past you.

Hook Type Fired when Parameters
buddynext_automatic_sanction_failed action An automatic sanction (strike, suspension, shadow-ban) could not be applied. Fires so a site can alert a human rather than let the moderation rule fail silently — the report stays open and nothing was applied. int $user_id, string $sanction, string $reason
buddynext_safeguard_check filter A post is about to be saved (create or edit), after the built-in automated checks pass true|WP_Error $result, int $user_id, string $content, string $link_url, string $context
buddynext_client_ip filter The safeguard service resolves the request IP for the blocked-IP check string $ip
buddynext_report_reasons filter The report reason list is built (default: spam, harassment, misinformation, inappropriate, fake, impersonation, other) string[] $reasons
buddynext_moderation_auto_actions filter A report has just been inserted, deciding which automated actions to apply (Free returns empty; Pro stacks actions here) array $actions, array $report

Note: $result is true when every built-in check passed. To block content, return a WP_Error. To allow it, return $result unchanged. Returning a non-WP_Error, non-true value is treated as allow. The pending_review error code is intentionally non-fatal upstream - callers save the post with a pending status rather than discarding it - so reserve WP_Error returns from your filter for content you genuinely want rejected.

Auto-action shapes for buddynext_moderation_auto_actions

Section titled “Auto-action shapes for buddynext_moderation_auto_actions”

Each entry in the returned array is an associative array with at least an action key. The supported shapes:

array( 'action' => 'remove', 'reason' => 'string' );
array( 'action' => 'warn', 'user_id' => 123, 'reason' => 'string' );
array( 'action' => 'suspend', 'user_id' => 123, 'reason' => 'string', 'duration_days' => 7 );

Pre-moderation holds a post for review before it appears. It is developer-only by design: there is no owner setting, and the retired buddynext_premod_mode option is deliberately not read. Honouring a stale stored value would leave a site silently holding posts with no UI to turn it off or to find them, so the mode comes from a filter or not at all.

Hook Type Default Parameters
buddynext_premod_mode filter 'off' string $mode - one of off, new_members, links, all
buddynext_premod_new_member_count filter 1 int $limit - how many of a new member’s first posts to hold

Hold the first three posts from every new member:

add_filter( 'buddynext_premod_mode', fn() => 'new_members' );
add_filter( 'buddynext_premod_new_member_count', fn() => 3 );

Held posts surface in the moderation queue’s Pending tab. Turning the mode back to off releases nothing on its own - anything already held still needs a decision there, which is the reason this is not a setting an owner can toggle blind.

These fire after a moderator (or an auto-action) acts on content or a member. Trust-and-safety integrations and gamification penalties hook these.

Hook Type Fired when Parameters
buddynext_report_created action A member submits a report int $report_id, string $object_type, int $object_id, int $reporter_id
buddynext_content_removed action Reported content is removed (by a moderator or an auto-action) string $object_type, int $object_id, int $actor_id
buddynext_post_auto_hidden action A post reaches the report threshold and is put “Under review” (reversible; not a takedown) int $post_id
buddynext_post_restored action An auto-hidden post is restored when its reports are cleared int $post_id, int $actor_id
buddynext_comment_hidden action A comment reaches the report threshold and is put “Under review” (the comment mirror of buddynext_post_auto_hidden) int $comment_id, int $actor_id (actor 0 = the automatic threshold)
buddynext_comment_restored action An auto-hidden comment is restored when its reports are cleared int $comment_id, int $actor_id
buddynext_user_warned action A member is issued a warning int $user_id, int $actor_id, string $reason
buddynext_strike_issued action A strike is recorded against a member int $strike_id, int $user_id, int $actor_id
buddynext_user_suspended action A member is suspended int $user_id, int $actor_id, string $reason, ?string $expires_at
buddynext_user_unsuspended action A suspension is lifted int $user_id
buddynext_member_suspended action Member-domain mirror of a suspension int $user_id, int $by_user_id
buddynext_member_unsuspended action Member-domain mirror of an unsuspension int $user_id, int $by_user_id from every call site, including the wp-admin Members screen. That screen used to fire $user_id alone, which killed a typed two-parameter listener with an ArgumentCountError; it now passes get_current_user_id() as the actor. No default is needed.
buddynext_user_shadow_banned action A member is shadow-banned (their content stays visible only to themselves) int $user_id, int $actor_id, string $reason
buddynext_user_shadow_ban_removed action A shadow ban is lifted int $user_id, int $actor_id
buddynext_appeal_submitted action A member appeals a moderation decision int $user_id, int $appeal_id, string $type, int $suspension_id
buddynext_appeal_resolved action An appeal is decided int $appeal_id, int $user_id, string $decision
buddynext_space_user_banned action A member is banned from a space int $space_id, int $user_id, int $banned_by
buddynext_space_user_unbanned action A space ban is lifted int $space_id, int $user_id

Note: Some events have two flavours. The buddynext_user_* suspension events are the moderation-domain canonical events; the buddynext_member_* variants are the member-domain mirrors fired from the admin Members screens. Hook whichever matches where you need to react; do not assume both fire on every code path.

Hook Type Fired when Parameters
buddynext_reporter_hidden_statuses filter Building a member’s feed, resolving which report statuses keep the reported content hidden from the member who reported it string[] $statuses, int $viewer_id

Reporting something removes it from the reporter’s own feed, and by default it stays gone for every report status - including dismissed and resolved.

That default is deliberate. Reporting is a statement about what the member wants in their own feed, and a moderator disagreeing about the content does not change what that member asked for. Dismissing a report therefore does not put the post back in front of the person who reported it. This matches Facebook, X and Instagram, and it matches the promise the UI already makes when it removes the card on report.

An owner who reads a dismissal as “we checked, it is fine, show it again” can drop dismissed (and/or resolved) from the list:

add_filter( 'buddynext_reporter_hidden_statuses', function ( array $statuses, int $viewer_id ): array {
// Treat a dismissed report as "no longer hidden" on this site.
return array_values( array_diff( $statuses, array( 'dismissed' ) ) );
}, 10, 2 );

This filter only affects the reporter’s own view. It never reveals content moderation has actually removed, and it never hides content from anyone else.

The admin moderation queue and the member-facing report modal expose theming seams so you can add columns, row actions, or panel content without forking the templates.

Hook Type Fired when Parameters
buddynext_mod_queue_columns filter The moderation-queue table header is built array $columns
buddynext_mod_queue_row_actions action A moderation-queue row’s action cell renders row context args
buddynext_moderation_queue_before action Before the moderation-queue list renders -
buddynext_part_member_report_modal_before / _after action Around the member report modal markup array $args
buddynext_part_member_report_modal_args / _classes filter Shape the report modal’s args / wrapper classes array $args / array $classes, array $args
buddynext_part_space_settings_panel_moderation_before / _after action Around the space moderation settings panel array $args

Tip: The _part_* modal and panel seams follow the same four-hook contract as every other BuddyNext template part (_before, _after, _args, _classes). For the full convention, see Hooks: Template Parts.

Hook Type Fired when Parameters
buddynext_2fa_enabled action A member turns on two-factor authentication int $user_id
buddynext_2fa_disabled action A member turns off two-factor authentication int $user_id
buddynext_2fa_required_roles filter Deciding whether 2FA is advised for a user (default: empty = advised for nobody) array $roles
buddynext_2fa_issuer filter Building the otpauth:// provisioning URI; sets the label shown in the authenticator app (default: site name) string $issuer

Authentication: registration and spam control

Section titled “Authentication: registration and spam control”
Hook Type Fired when Parameters
buddynext_spam_protection_enabled filter Deciding whether registration spam protection runs bool $enabled
buddynext_register_rate_limit filter Resolving the max registrations per window (default from option buddynext_reg_rate_limit) int $max
buddynext_registration_spam_score filter Scoring a registration attempt; higher is spammier int $score, array $ctx
buddynext_registration_blocked action A registration is rejected as spam array $ctx, int $score
buddynext_registration_honeypot_field filter The honeypot field name on the sign-up form (default bn_website) string $field
buddynext_registration_allowed_domains filter The email-domain allowlist for sign-up array $allowed, string $email
buddynext_disposable_domains filter The disposable-email-domain blocklist array $domains
buddynext_registration_challenge_enabled filter Whether a registration challenge (captcha-style) is shown bool $on
buddynext_registration_pending action A new account is created but awaits verification or approval int $user_id, string $email
buddynext_registration_fields_saved action Custom registration field values are stored int $user_id, array $values, array $fields
buddynext_member_approved action A pending registration is approved int $user_id

Authentication: email verification and social login

Section titled “Authentication: email verification and social login”
Hook Type Fired when Parameters
buddynext_send_verification_email action A verification email is about to be sent int $user_id, string $token_url
buddynext_user_verified action A member completes verification int $user_id
buddynext_email_verified action A member’s email address is confirmed via the verify link int $user_id
buddynext_email_change_requested action A member requests an email-address change (pending confirmation) int $user_id, string $new_email
buddynext_email_changed action An email-address change is confirmed int $user_id, string $new_email
buddynext_oauth_providers filter The OAuth provider definitions are assembled array $providers
buddynext_auth_social_providers filter The social provider buttons rendered on login / signup / connected-accounts array $providers
buddynext_social_icon filter A social-LOGIN provider’s button icon is resolved (login / signup / connected accounts). It does NOT affect the profile hero’s social-link chips - those icons are a hardcoded map with no filter string $icon, string $provider_id
buddynext_social_user_created action A new account is created from a social login int $user_id, string $provider_id, array $profile

Authentication: private community access (1.0.7)

Section titled “Authentication: private community access (1.0.7)”
Hook Type Fired when Parameters
buddynext_private_community_can_access filter Deciding whether the current visitor may access the community, while Private Community mode is on (Settings → Privacy & Data → Private Community) - gates both the hub-page redirect and the REST 401 response bool $can_access (default: is_user_logged_in())

PrivateCommunity::can_access() is the single access seam for membership plugins. By default it grants access to any logged-in visitor. A membership plugin (Paid Memberships Pro, WP Fusion, MemberPress) filters this to decide on its own terms - for example, requiring an active plan or a required tag, not just a login. Return false to send a visitor to the login (or upgrade) page like a guest; return true to grant access. The filter only runs when Private Community mode is enabled - it has no effect while the community is public.

// Require an active "pro" membership tag, not just a login, once Private
// Community mode is on.
add_filter(
'buddynext_private_community_can_access',
function ( bool $can_access ): bool {
if ( ! is_user_logged_in() ) {
return false;
}
return (bool) wpf_has_tag( get_current_user_id(), 'pro-member' );
}
);

Authentication: login and logout redirects (1.0.7)

Section titled “Authentication: login and logout redirects (1.0.7)”
Hook Type Fired when Parameters
buddynext_redirect_url filter Resolving the login / logout / onboarding redirect URL, after the owner’s Settings > Registration & Login value (or the built-in default) has already been applied - runs last, so it has the final say string $url, string $context, string $fallback
buddynext_content_removal_handled filter Moderation asks whether reported content was actually taken down. Core answers for post, comment and message; return true for an object type you own bool $handled, string $object_type, int $object_id, int $actor_id
buddynext_private_community_exempt_routes filter The private-community gate resolves which route prefixes are exempt, for callers that are legitimately never logged in string[] $exempt, WP_REST_Request $request
buddynext_terms_consent_recorded action A member’s terms consent is recorded. Hook it if you owe a stricter compliance duty, such as a written audit log or an external consent store int $user_id, int $terms_page, string $source

RedirectSettings::resolve() is the one place every login, logout, and onboarding redirect passes through - the BuddyNext auth hub, wp-login.php, a theme login form, and programmatic sign-in all resolve through it. $context is one of 'login', 'logout', or 'onboarding'; $fallback is the built-in default for that context (for example, the activity feed for login). Send a member to a custom dashboard or an external portal without touching the settings UI:

add_filter(
'buddynext_redirect_url',
function ( string $url, string $context, string $fallback ): string {
if ( 'login' === $context ) {
return home_url( '/dashboard/' );
}
return $url;
},
10,
3
);

Note: WordPress’s own wp_safe_redirect() at the apply point is the safety net - an off-site target still needs allowed_redirect_hosts or it falls back to $fallback.

Add a custom safeguard via buddynext_safeguard_check

Section titled “Add a custom safeguard via buddynext_safeguard_check”

The example below blocks any post that contains more than two links, on top of BuddyNext’s built-in checks. Because the filter runs on both create and edit, this rule applies to edited posts too.

add_filter(
'buddynext_safeguard_check',
function ( $result, $user_id, $content, $link_url ) {
// Respect any earlier rejection (built-in check or another plugin).
if ( is_wp_error( $result ) ) {
return $result;
}
// Trusted roles bypass the rule.
if ( user_can( $user_id, 'manage_options' ) ) {
return $result;
}
// Reject posts with more than two URLs - a common spam pattern.
if ( preg_match_all( '#https?://#i', $content ) > 2 ) {
return new WP_Error(
'too_many_links',
__( 'Posts can include at most two links. Please remove some and try again.', 'my-ext' )
);
}
return $result;
},
10,
4
);

Warning: Always return $result unchanged when you do not want to block, and short-circuit on an existing WP_Error so you do not mask a rejection from a higher-priority rule (including BuddyNext Pro’s Moderation Rules). Returning true unconditionally would silently override every other safeguard.

Penalise the recipient on a strike (gamification)

Section titled “Penalise the recipient on a strike (gamification)”
add_action(
'buddynext_strike_issued',
function ( $strike_id, $user_id, $actor_id ) {
// Deduct trust points from the struck member.
my_gamification_adjust_points( $user_id, -50, 'moderation_strike' );
},
10,
3
);

Reject sign-ups from a disposable-email domain you maintain

Section titled “Reject sign-ups from a disposable-email domain you maintain”
add_filter(
'buddynext_disposable_domains',
function ( $domains ) {
$domains[] = 'throwaway.example';
return $domains;
}
);
  • The safeguard filter does not see the post ID - the post has not been saved yet on create. If you need post context, key your logic off $user_id and $content.
  • buddynext_moderation_auto_actions is empty in Free. Returning actions from it is how Pro (or you) drive automatic remove/warn/suspend off a report. Each action you add executes immediately after the report row is written, so guard against double-acting on the same report.
  • Suspension and member events come in pairs. Listen on the event that matches your trigger surface (moderation queue vs admin Members screen); do not register the same handler on both expecting one fire.
  • 2FA is never a hard gate. If your integration must enforce 2FA, enforce it in your own login flow - BuddyNext only surfaces it as advisory via buddynext_2fa_required_roles.
  • Free vs Pro. Every hook on this page is Free. BuddyNext Pro adds no new safeguard seam - it extends the same buddynext_safeguard_check and buddynext_moderation_auto_actions filters, which is exactly why your custom rules and Pro’s rules engine coexist on one pipeline. For the search and admin seams, see Hooks: Search, Hashtags, Sidebar, and Admin.