Automatically hide spam, scam and link-drop comments on your Facebook Page posts. CommentHide is an open-source Facebook comment moderation tool that runs as a single Cloudflare Worker in your own account. A cron trigger checks your watched posts every minute, evaluates each new comment against a rule engine you control, and hides only what actually matches — through the official Meta Graph API.
No SaaS subscription. No third party ever sees your Page token or your comments.
If you run Facebook Ads, you know the pattern. A post starts performing, and within an hour the comments fill up with crypto bait, "check my profile" follower farms, WhatsApp numbers and competitors dropping links. Every one of them costs you clicks and social proof, and Facebook gives you no way to filter them automatically.
The tools that solve this are subscriptions — €30 to €200 a month, per Page, with your Page token living on someone else's server.
CommentHide does the same job on infrastructure you already own. Cloudflare's free tier covers a minute-by-minute cron trigger and a D1 database comfortably, so running it typically costs nothing.
| Hosted moderation SaaS | CommentHide | |
|---|---|---|
| Cost | €30–200 / month, per Page | Free tier on Cloudflare |
| Where your Page token lives | Vendor's database | Your Cloudflare account, AES-256-GCM encrypted |
| Who can read your comments | Vendor | Nobody but you |
| Rules | Vendor's presets | Yours, seven rule kinds, priority ordered |
| Preview before it acts | Rarely | Dry run on every post |
| Auditability | A vendor dashboard | Every decision, with its reason, in your own database |
| Undo | Sometimes | One click, per comment or in bulk |
| Source | Closed | MIT |
Most "hide Facebook comments" scripts hide everything new. That is a blunt instrument — it buries genuine questions and honest criticism along with the spam, and it is the reason this category has a bad reputation.
CommentHide evaluates every comment against your rule set and records why it reached each verdict. Here is the comment inspector on a post in dry run — the shipped starter rules, nothing customised, and nothing yet touched on Facebook:
The crypto bait, the emoji flood and the follower farm are all marked Would hide, each labelled with the rule that caught it. "Honestly the last bag was stale and shipping took nine days. Disappointed." is marked Would keep, because no rule matched it — and a moderation tool has no business hiding that.
That column on the right is the part worth dwelling on. It shows what the next check would actually do, not what the rule set says in the abstract: a comment the tool has already settled reads Already decided rather than promising an outcome that will never happen. Edit a rule, reload, and see the consequence before it reaches Facebook.
Leave the post in dry run for as long as you like. Switch dry run off and the very next check acts on exactly what the preview showed you.
- Rule engine, seven kinds — keyword, regex, link, contact details, emoji flooding, minimum length, and an author allowlist that overrides everything else.
- Dry run — a watched post can evaluate and record every verdict while writing nothing to Facebook. Run it for a day, read the ledger, then switch it on for real.
- Starts from now — activating a post records every comment already on it as seen, so switching CommentHide on can never touch existing conversation. It is the default, it pages to the end rather than sampling, and a post whose baseline did not complete is refused by every code path that could poll it — cron, Run now, or the Active toggle.
- Multiple posts — watch as many as you like, each with its own mode and rules.
- Replies — optionally includes nested replies, at no extra API cost: the comments edge returns them flattened when asked.
- Full audit trail — every decision stored with its matched rule and reason.
- One-click undo — restore a single comment, or every comment the tool hid on a post.
- Token encrypted at rest — AES-256-GCM. It is never returned to the browser, never written to a log, and never placed in a URL.
- Hardened by default — CSP, CSRF double-submit tokens, rate-limited login, HMAC-signed session cookies.
- Automatic retention — old audit rows are pruned on a schedule you set.
- Offline development — a bundled mock Graph API lets you run the whole thing without a Facebook App, a Page, or a token.
A Cloudflare Cron Trigger wakes the Worker every minute. For each active post it resolves the target once, fetches the latest comments from the Graph API, loads every already-seen comment id in a single query, and evaluates only what it has never decided on before.
A comment is processed exactly once. The comments table is both the idempotency ledger
and the audit trail, which is what makes the tool safe to leave running: it cannot hide
the same comment twice, and it cannot forget what it did.
Allow rules are evaluated first, always, so an allowlist can never be outranked by a higher-priority hide rule. Then the remaining rules run in priority order and the first match wins. The verdict, the rule that produced it and a human-readable reason are all written to the ledger.
| Rule kind | Pattern | Hides when |
|---|---|---|
keyword |
comma-separated terms | a term appears as a whole word — case- and accent-insensitive, so perche matches PERCHÈ |
regex |
JS regex source | the expression matches; an invalid one is skipped, never fatal |
link |
— | the text contains a URL, a bare domain, or an obfuscation like example(dot)com |
contact |
— | it contains a phone number, an email address or an @handle |
emoji_spam |
threshold — starter rule uses 6, falls back to 5 | the comment carries that many emoji or more |
min_length |
threshold — starter rule uses 2, falls back to 3 | the trimmed comment is shorter than that |
author_allow |
names or ids | the author matches — pair with the allow action |
Every rule carries an action (hide, flag or allow) and a priority. flag records a
verdict and never writes to Facebook, which makes it a good way to trial a new rule on a
live post without consequences.
You need a Cloudflare account (the free plan is enough), Node 22+, and a Facebook Page you administer.
git clone https://github.com/Hiberius/commenthide-facebook-comment-moderation.git
cd commenthide-facebook-comment-moderation
npm install
npm run setupnpm run setup checks your toolchain, creates wrangler.toml from the example, and
generates the two random keys you need — printing the exact commands to run. Then:
npm run db:create # prints your D1 database_id -> paste it into wrangler.toml
npm run db:apply:remote # create the schema
npx wrangler secret put ADMIN_PASSWORD
npx wrangler secret put ENCRYPTION_KEY
npx wrangler secret put SESSION_SECRET
npm run deployOpen the Worker URL, sign in with ADMIN_PASSWORD, paste a Page Access Token, add a
post, and leave it in dry run for a while before switching it on.
CommentHide needs a Page Access Token — not a personal user token — with these permissions:
pages_read_engagement— read the post and its commentspages_manage_engagement— setis_hiddenon a comment
Create an app at developers.facebook.com, add the Facebook Login for Business or Pages API product, then use the Graph API Explorer to issue a token for the Page you administer. Exchange it for a long-lived token so it does not expire in an hour — see Meta's Page access token guide.
If you paste a user token that manages the Page, CommentHide resolves the correct Page
token for you automatically through /me/accounts.
Secrets — set with wrangler secret put, never committed:
| Secret | What it is |
|---|---|
ADMIN_PASSWORD |
Dashboard login. Use a long random passphrase. |
ENCRYPTION_KEY |
base64 of 32 random bytes. openssl rand -base64 32 |
SESSION_SECRET |
Session cookie signing key. openssl rand -base64 48 |
Public variables in wrangler.toml:
| Variable | Default | What it does |
|---|---|---|
GRAPH_API_VERSION |
v25.0 |
Graph API version segment |
RETENTION_DAYS |
30 |
Prune audit events older than this. 0 disables it. The comment ledger is never pruned: every row in it carries a standing decision, and deleting one would make the next check re-decide a comment it had already settled. |
GRAPH_API_BASE |
unset | Development only. Points the client at a mock instead of Meta. |
The threat this project takes seriously is your Page Access Token, because it is the one secret that could be used against you.
- Encrypted at rest. AES-256-GCM with a random 12-byte IV per encryption, using a key that lives only as a Cloudflare secret.
- Write-only from the browser. The dashboard can set the token and can never read it
back — not even masked.
/api/statusreturns a boolean, not a value. - Never in a URL. Graph reads carry it as a
Bearerheader and writes carry it in the form body, so it cannot land in a proxy log or an error report. - Never in a log. Every message that leaves the Graph client passes through a redaction pass first.
- Sessions are HMAC-SHA256 signed cookies with
__Host-prefix,HttpOnly,Secure,SameSite=Strictand a seven-day lifetime. - Login is rate limited — eight failures inside fifteen minutes locks that client for fifteen minutes, and a locked client is refused before the password is ever compared.
- CSRF is a double-submit token required on every state-changing request.
- CSP is
default-src 'none'with no external origin allowed. The dashboard loads no web font, no CDN script and no analytics; the entire UI is one self-contained document. - SQL is fully parameterised; the two dynamic
UPDATEstatements build column names from literal allow-lists, never from input. - Comment text is never treated as markup. The dashboard builds DOM nodes with
textContent, so a comment containing HTML is displayed, not executed.
Found something? Please open a private security advisory — see SECURITY.md.
This tool hides comments on posts you own, through an API Meta provides for exactly that purpose. Hidden comments are not deleted: they stay visible to their author and to that author's friends, which is how Facebook's own hide function behaves.
It is built to remove spam, scams, harassment and off-topic noise. It is not built to bury criticism, and the defaults reflect that — the starter rules match link drops, contact-detail spam, emoji floods and known scam phrasing, and nothing that resembles an unhappy customer. The dry run, the stated reason on every verdict and the one-click undo all exist so you can check that for yourself rather than take it on trust.
Using it to silence legitimate feedback is a choice you would be making, not one this project makes for you. Please also read Meta's Platform Terms — you are responsible for your own compliance.
You do not need a Facebook App to work on CommentHide. A mock Graph API ships with it:
npm install
cp .dev.vars.example .dev.vars # then fill in the three values
npm run db:apply:local
npm run mock:graph # terminal 1 — fake Graph API on :8788
npm run dev:mock # terminal 2 — the Worker, pointed at the mockSign in, paste any non-empty string as the token, and add
100000000000001_200000000000001 as a post. The mock serves a deliberately mixed comment
thread — spam, borderline cases and honest criticism — which is what the screenshots in
this README were captured from.
Cron triggers do not fire under wrangler dev, so use Run now in the dashboard to
exercise the poll loop.
npm run typecheck
npm test
npm run test:coverage313 tests currently pass. The rule engine is pure and covered to 100% of statements; the
Graph client is tested against an injected fetch and never touches the network.
HTTP API reference
Every route below requires a session cookie, and every non-GET route requires the
x-csrf-token header. POST /api/session and GET /health are the two exceptions.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/session |
Sign in; returns the CSRF token |
DELETE |
/api/session |
Sign out |
GET |
/api/status |
Dashboard state, totals and watched posts |
PUT |
/api/page/token |
Verify and store the Page Access Token |
DELETE |
/api/page/token |
Forget the stored token |
GET |
/api/page/posts |
Recent posts on the connected Page |
GET POST |
/api/posts |
List, or start watching a post |
PATCH DELETE |
/api/posts/:postId |
Update or stop watching |
GET |
/api/posts/:postId/comments |
Live comments with current and would-be verdicts |
POST |
/api/posts/:postId/test |
Verify token and post reachability |
POST |
/api/posts/:postId/restore |
Unhide everything the tool hid on that post |
GET POST |
/api/rules |
List or create a rule |
PATCH DELETE |
/api/rules/:id |
Update or delete a rule |
POST |
/api/rules/seed |
Install the starter rule set |
POST |
/api/run |
Run the poll now, optionally as a dry run |
POST |
/api/comments/:commentId/hide |
Hide one comment manually |
POST |
/api/comments/:commentId/show |
Unhide one comment |
GET |
/api/events |
Recent audit events |
GET |
/health |
Public health check |
Project layout
src/
├── index.ts Hono app, middleware, cron handler
├── types.ts every shared type — the single source of truth
├── lib/
│ ├── crypto.ts AES-256-GCM, redaction, constant-time compare
│ ├── auth.ts sessions, CSRF, login throttling
│ ├── security.ts CSP and security headers
│ ├── graph.ts Meta Graph client — memoised target resolution
│ ├── graph-http.ts transport: auth placement, retry, backoff
│ ├── graph-parse.ts pure parsing and error copy
│ ├── rules.ts the rule engine — pure, no I/O
│ ├── poller.ts the poll loop
│ ├── poll-comment.ts per-comment decision and action
│ ├── retention.ts scheduled pruning
│ └── storage/ one D1 module per table
├── routes/ one Hono sub-app per resource
└── ui/ the dashboard — markup, styles, client script
Every file is under 400 lines, on purpose.
- Meta Webhooks for sub-second hiding, with cron as the fallback
- Rule templates you can import and share
- CSV export of the audit ledger
- Optional Slack or email digest of what was hidden
- Per-post rule overrides in the dashboard
Issues and pull requests are welcome — see CONTRIBUTING.md.
Does this delete comments? No. It only sets is_hidden, the same flag as Facebook's
own Hide button. Nothing is ever deleted, and every hide is reversible from the dashboard.
How fast does it hide a comment? Within a minute, on the next cron tick. Webhooks are on the roadmap for near-instant hiding.
Does it work on Instagram comments? Not yet. The Instagram Graph API exposes a comparable endpoint, and the client is structured to allow it.
Will this get my Page banned? It uses documented Graph API endpoints with the permissions Meta grants for exactly this purpose. Keep within the rate limits — the default of one poll a minute is well inside them.
Can I watch more than one post? Yes, as many as you want, each with its own mode, dry-run setting and reply behaviour.
What does it cost to run? On Cloudflare's free plan, typically nothing. One poll a minute is roughly 43,000 requests a month against a 100,000/day free allowance, and the D1 free tier covers the storage many times over.
How many posts can one deployment watch? The real constraint is Cloudflare's cap of 50 outbound subrequests per invocation on the free plan (1000 on paid). CommentHide budgets its reads so a run always keeps requests in hand for the hides themselves, and warns in the activity log rather than walking into the limit — but on the free plan, plan for a handful of busy posts rather than dozens. The paid Workers plan removes the constraint entirely.
Does anything leave my infrastructure? Only the calls to Meta's Graph API. There is no telemetry, no analytics and no third-party request of any kind.
Can I try it without a Facebook App? Yes — npm run mock:graph runs a complete
offline Graph API.
I build tools like this one — performance marketing systems, internal automation, and products on the Cloudflare edge stack. If you need something similar built properly, I take on freelance and contract work.
Christian Calabro — github.com/Hiberius
Performance marketing · media buying · TypeScript · Cloudflare Workers · Next.js · Python
MIT © 2026 Christian Calabro
CommentHide is an independent open-source project. It is not affiliated with, endorsed by, or sponsored by Meta Platforms, Inc. Facebook is a trademark of Meta Platforms, Inc.