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
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
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
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.
