Home Blog

Anyone Can Type Here: The 3 Hooks That Decide What a WordPress Comment Becomes

The Problem A comment form is the one place on most WordPress sites where an anonymous visitor’s text ends up stored in the database and rendered back to every other visitor. A…

The Problem

A comment form is the one place on most WordPress sites where an anonymous visitor’s text ends up stored in the database and rendered back to every other visitor. A developer who treats that as “just another piece of content” — trusting whatever comes through, and only lightly touching it in comment_text for display — is one crafted comment away from a stored cross-site scripting issue, because by the time comment_text runs, the raw value is already sitting in the database exactly as submitted.

Why This Is Dangerous

These three hooks fire at different, easy-to-confuse points in a comment’s life: wp_insert_comment fires after a comment already exists as a database row, comment_post fires right after a comment is submitted through the standard front-end form (a narrower case than “any comment insert”), and comment_text fires only at render time — on every single display, not once at submission. Sanitizing only in comment_text protects the visible page but leaves the raw, unsanitized value in the database, accessible to anything else that reads comments directly — a REST endpoint, an export tool, a search index. And relying on comment_text to “catch” malicious input treats an output filter as if it were an input validator, which it was never designed to be.

Step-by-Step Solution — The Contract of Each Hook

`wp_insert_comment` (action) — a comment already exists; this fires for ALL of them

Plaintext
add_action( 'wp_insert_comment', function( $comment_id, $comment ) {
	// Fires for front-end submissions, programmatic inserts, and imports alike —
	// don't assume this comment came through the public comment form.
	forwp_notify_moderators_if_flagged( $comment_id );
}, 10, 2 );

This is the broadest of the three — it fires whether the comment came from a real visitor, a REST API request, an import script, or wp_insert_comment() called directly by another plugin. Logic that should only apply to genuine front-end submissions doesn’t belong here unmodified.

`comment_post` (action) — specifically after a front-end submission is saved

Plaintext
add_action( 'comment_post', function( $comment_id, $comment_approved, $comment_data ) {
	if ( 1 === $comment_approved ) {
		forwp_send_author_notification( $comment_id );
	}
}, 10, 3 );

comment_post is the narrower, front-end-specific hook — the right place for “email the post author when a real visitor comments,” since it only fires for comments that actually went through wp-comments-post.php. The $comment_approved value tells you the moderation outcome (1 approved, 0 held for moderation, 'spam' marked as spam) at the moment of submission, not necessarily its current status.

`comment_text` (filter) — output only, and it runs on every display

Plaintext
add_filter( 'comment_text', function( $comment_text, $comment ) {
	// This is presentation logic — the actual sanitization has to happen
	// BEFORE the comment is ever saved to the database, not here.
	return forwp_add_read_more_link_to_long_comments( $comment_text );
}, 10, 2 );

Because comment_text runs at render time, it fires once per display, not once per comment — a popular comment shown on ten pages runs this filter ten separate times per page load across visitors. It’s the wrong layer to rely on for security: the actual defense against malicious comment content is sanitizing on the way in (WordPress’s own wp_filter_comment() and the comment moderation pipeline), not filtering on the way out.

When It’s Better to Bring In a Specialist

Comment security is one of the few areas of WordPress where “it displays fine” and “it’s actually safe” are genuinely different questions — a payload that never renders visibly can still sit in the database and surface somewhere else entirely, like a REST response or a moderation email rendered in HTML. An agency team that has hardened comment handling before checks the full submission-to-render pipeline, not just the visible output, and that kind of dedicated WordPress team review is exactly what a comment-heavy community site or membership platform needs before the volume of user-generated content makes a manual audit impractical.

FAQ

No. comment_text only affects what’s displayed at render time — the raw value is already stored in the database by the time this filter runs. Real protection has to happen at submission, before the comment is inserted.

comment_post fires specifically when a comment is submitted through the standard front-end comment form. wp_insert_comment fires for every comment insert regardless of source — front-end submissions, programmatic inserts, imports — so it’s the one to use when you need to catch all of them.

No — it reflects the moderation outcome at the moment comment_post fires. A comment can be manually approved or marked as spam afterward by a moderator, and that later change doesn’t re-fire comment_post.

Because it’s an output filter that runs every time the comment is rendered, not once when it’s created. The same comment shown on a paginated list and again on its own permalink page runs the filter twice, independently.

Yes — any code that calls wp_insert_comment() directly, including import tools, migration scripts, and other plugins, triggers this hook exactly as if a visitor had submitted the comment.

Before the volume of user-generated content makes manual review impractical — a community site, a membership platform, or any site where comments are a primary engagement feature benefits from a security pass on the full submission pipeline, not just the rendered output.

Summary

A full breakdown of each of the 3 hooks in this category — with examples and a hands-on IDE — is on the Comments Hooks page, and the whole set is also available as one PDF to keep.