Accessible honeypot
Off-screen, aria-hidden, tabindex="-1" and autocomplete="off", with a randomised, realistic name per build.
npm package · MIT · Node ≥ 18
Block form spam bots without a captcha.
Layers cheap checks that most bots fail and real people never notice: an accessible honeypot, an HMAC-signed timing token, a JS proof, interaction signals, content heuristics, rate limiting and optional Altcha-style proof-of-work. webpack and Vite plugins, a <2 kB client, and server helpers for Express, Fastify, Next.js, SvelteKit, Remix, Astro and any Fetch runtime.
npm i form-honeypot-guard
$ npx form-honeypot-guard check dist dist/contact.html (2 forms) ✖ error POST form without a honeypot [fg/no-honeypot] form at line 1 (action=/c) posts without a honeypot field fix: add data-guard to the form and build with FormGuardPlugin ✖ error GET form sends personal data in the URL [fg/get-personal-data] form at line 1 (action=/news) uses GET and sends email in the query string fix: add method="post"
Captchas cost you conversions and accessibility, and most contact-form spam comes from dumb, untargeted bots that a few invisible checks would stop.
form-honeypot-guard adds those checks at build time and verifies them on the server: bots that fill every field, submit instantly, skip JavaScript, replay tokens or flood you get rejected, and people never see a puzzle.
Off-screen, aria-hidden, tabindex="-1" and autocomplete="off", with a randomised, realistic name per build.
HMAC-SHA256 tokens reject submissions that are too fast, expired, tampered with or replayed.
A proof only a real browser writes, plus keypress, pointer and focus counts.
Sliding window per IP, email or fingerprint, with a correct 429 and Retry-After.
Altcha-style, privacy-friendly PoW with no captcha vendor.
webpack 5 and Vite plugins add the markup to built HTML and inject the ~1.3 kB gzipped runtime.
Express, Fastify, Next.js, SvelteKit, Remix, Astro, Web Fetch runtimes, and a React wrapper.
Bring your own Redis, Upstash, Cloudflare KV or Durable Object client. Nothing bundled.
# Generate an HMAC secret
npx form-honeypot-guard secret# Audit built HTML forms
npx form-honeypot-guard check dist# Strict audit (warnings fail)
npx form-honeypot-guard check dist --strict# JSON output
npx form-honeypot-guard check dist --jsonimport express from 'express';
import { expressGuard, cleanFields } from 'form-honeypot-guard';
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(expressGuard({ manifest: 'server/form-guard.manifest.json', paths: ['/contact'] }));
app.post('/contact', (req, res) => {
sendEmail(cleanFields(req.body, { manifest: 'server/form-guard.manifest.json' }));
res.redirect('/thanks');
});
# .github/workflows/forms.yml
name: Forms
on: [push, pull_request]
jobs:
forms:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci && npm run build
# exit 1 if a built form is missing protection
- run: npx form-honeypot-guard check dist
| Flag | Description |
|---|---|
check [dir] | Audit built HTML forms (default: dist) |
check --json | JSON output |
check --strict | Warnings count as errors |
secret [--env] | Print a new HMAC secret |
forms (plugin) | Selector or 'all' (default 'form[data-guard]') |
minTime / maxAge | 3000 / 7200000 ms |
requireJs | true, 'soft' (adds score only) or false |
threshold (server) | Score at which a submission fails (default 5) |
rateLimit (server) | { limit: 5, windowMs: 60000, keys: ['ip'] } or false |
silent (server) | Fake 200 {"ok":true} for rejected bots; rate limits still return 429 |
pow (server) | true or { maxnumber, require: true | 'soft', field } |
trustProxy (server) | true, a number of hops, or a list of proxy IPs |
Full reference, config file format and API in the README.
Support this project
I build free, open-source tools that help developers ship safer websites. Every coffee helps keep them maintained, documented and free for everyone.
Buy me a coffee (opens Stripe in a new tab)Secure checkout by Stripe