Banned & restricted hashtag checker for Instagram. (Formerly HashDetector.)
CleanHashtags tells you which of your Instagram hashtags are known to be banned or restricted, so you can drop them before you post. Paste up to 30 hashtags and get a verdict for each one, with where that verdict comes from and how old it is.
It runs on Cloudflare's free Workers plan: one Worker serves the React app and a small JSON API backed by a D1 database.
Instagram has no public "is this hashtag banned?" endpoint, and the page the original 2018 version scraped no longer shows the signal. CleanHashtags therefore looks tags up in a curated list, and every entry carries its evidence:
| Status | Meaning |
|---|---|
banned |
Verified first-hand: Instagram hides the tag. |
restricted |
Verified first-hand: the tag works but Instagram limits it. |
reported |
Listed by others (for example old "banned hashtag" lists) but not verified. |
clear |
Verified first-hand as usable. |
unknown |
Not in the list. This is not a "clear"; it means we have no data on it. |
Each result shows its source and, when the tag was verified, how many days ago. "Copy safe hashtags" copies only clear and unknown tags.
Limits worth knowing:
- The list is small. Today every entry is
reported, taken from 2019–2020 community lists, and none has been verified by this project. - Instagram changes its rules without notice, so even a verified verdict goes stale.
- A live check through the Instagram Graph API (
ig_hashtag_search) is possible but limited to 30 unique hashtags per account per 7 days and needs Meta app review. Only a stub exists (apps/api/src/verifier), and it is off by default.
The reasoning behind this approach, including the 2026 calibration that ruled out scraping, is in docs/adr/0001-stack-and-data-source.md.
Requirements: Node 22.18 or newer (see .nvmrc; the seed script runs TypeScript directly with Node) and pnpm (the version is pinned in package.json; corepack enable picks it up). No Cloudflare account is needed for local development.
pnpm i
pnpm --filter api db:migrate:local # create the local D1 schema
pnpm --filter api db:seed # load seed/hashtags.json (re-runs migrations; safe to repeat)
pnpm dev # API on :8787, web app on Vite's port (usually :5173)Open the Vite URL; it proxies /api to the Worker. Other scripts, all from the repo root:
| Command | What it does |
|---|---|
pnpm lint |
ESLint and Prettier check |
pnpm typecheck |
tsc --noEmit in every package |
pnpm test |
Vitest in every package (API tests run in workerd) |
pnpm build |
Builds the web app to apps/web/dist |
pnpm format |
Prettier write |
Everything below fits Cloudflare's free plan on the *.workers.dev subdomain. Do not add a custom domain or paid products.
cd apps/api
pnpm exec wrangler login
pnpm exec wrangler d1 create cleanhashtagsCopy the printed database_id into the d1_databases entry in apps/api/wrangler.jsonc, then:
pnpm db:seed:remote # applies remote migrations, then seeds
pnpm run deploy # `run` is required: `pnpm deploy` is a built-in pnpm command
# builds apps/web and publishes to https://cleanhashtags.<your-subdomain>.workers.devALLOWED_ORIGINS in wrangler.jsonc can stay empty because the app and API share an origin. Keep GRAPH_API_ENABLED at "false".
apps/api Cloudflare Worker (Hono): /api routes, D1 access, rate limiting, verifier stub
migrations/ D1 schema
seed/ starter hashtag list and its provenance
apps/web React + Vite frontend, served by the Worker as static assets
packages/shared hashtag parsing and Zod schemas used by both apps
docs/ PLAN.md, ADRs and React learning notes
The frontend was built step by step as a React learning project. Each step has a note explaining the concepts it introduced, where to find them in the code, and exercises to try:
- Components and JSX
- State and controlled inputs
- Events, async and effects
- Lists, keys and lifting state
- Custom hooks and styling
Planned improvements, roughly in priority order. Everything here must keep hosting at $0.
- Deploy on merge to
master. Add a deploy job usingcloudflare/wrangler-actionwith a scopedCLOUDFLARE_API_TOKENsecret. The job applies remote D1 migrations, then deploys. Today deploys are run by hand from a laptop. - Know when new code is live. Deploy through a GitHub
productionenvironment whose URL ishttps://cleanhashtags.didagou.workers.dev, so each deployment shows up on the repo and its PR with a "View deployment" link. AddGET /api/versionreturning the deployed git SHA and build time, and show it in the page footer. - Post-deploy smoke test. After deploying, poll
/api/versionuntil it reports the new SHA, then check/api/healthand a/api/checkrequest. Fail the workflow, and so the deployment status, if they don't pass. - Preview deployments per PR. Upload a Worker version with
wrangler versions uploadon every PR and comment its preview URL on the PR (free on Workers). - Make CI a required check. Add the
checkjob as a required status check in theprotect-default-branchruleset, so red PRs can't be merged now that approvals are no longer required. - Faster, clearer CI. Split lint, typecheck, test and build into parallel jobs, and upload test coverage as a report.
- End-to-end test. Add a Playwright smoke test that loads the app, checks a few hashtags and copies the result. Run it in CI and against preview URLs.
- Dependabot hygiene. Auto-merge passing minor and patch updates. Keep
@types/nodein step with the runtime (.nvmrcis Node 22 while@types/nodeis 26), either by moving the runtime to the current Node LTS or by ignoring@types/nodemajors. - Releases and changelog. Use release-please (or similar) to tag releases and keep a
CHANGELOG.mdfrom conventional commits.
- Turn on secret scanning, push protection and Dependabot security updates, which are currently disabled, and add CodeQL scanning.
- Add
SECURITY.mdwith a way to report vulnerabilities. - Uptime check: a scheduled GitHub Actions job that hits
/api/healthand opens an issue if it fails. - Watch for abuse in Workers Logs. If needed, add Cloudflare Turnstile (free) in front of
/api/checkand tune the rate limit (20 requests per minute per IP).
- Verify the seed list. All 24 seed tags are
reported(unverified, from 2020-era lists). Check each one, recordlast_verified_at, and promote or drop it. - List curation workflow. Accept list changes through a GitHub issue template that asks for evidence, plus a script that applies approved changes to D1.
- Live verification. Implement
GraphApiVerifieragainstig_hashtag_search(needs an Instagram Business/Creator account and Meta app review), with users connecting their own account. Every verified result should update the shared list.
- Suggest replacement hashtags for banned or restricted ones.
- SEO basics: Open Graph and Twitter meta tags, a share image,
robots.txt,sitemap.xml. - Privacy-friendly usage stats with Cloudflare Web Analytics (free, no cookies).
- Accessibility audit (keyboard, screen reader, contrast) and a Lighthouse check in CI.
- Browser extension that checks hashtags inside Instagram's caption box, using the same API.
- Move to TypeScript 7 once
typescript-eslintsupports it (held at 6.0 today). - Issue and PR templates, plus
CODEOWNERS. - Learning notes: replace line-number references with permalinks to the commit each note was written for, so they don't go stale.
- Optional, not free: a custom domain (
cleanhashtags.comwas unregistered as of October 2026).
See CONTRIBUTING.md, including how to propose changes to the hashtag list.