A CloudFront Function (JS 2.0 runtime) designed to protect a static website hosted on AWS S3 (no PHP). It filters incoming requests before they reach the origin or cache.
Requests with no User-Agent header, an empty value, or whitespace-only value return 404. This check runs first and cannot be bypassed.
/ads.txt and /robots.txt bypass all further checks and return unchanged (still requires a non-empty user-agent).
Requests matching automated-scan patterns return 404:
- URI extensions:
.php*,.sql,.bak,.phtml,.phar - Common scanner folders:
/admin,/wp-admin,/phpmyadmin,/backup, etc. - Sensitive paths:
/.env,/.git,/ip
Requests with mismatched rv: and firefox/ versions return 404.
Requests matching known bot/scraper user-agent patterns return 404, on every path.
Blocked patterns include: scrapers (Scrapy, PetalBot, DataForSEO, etc.), old browser tokens (Trident, Presto), and 50+ other known bots/crawlers, matched case-insensitively against the User-Agent header.
All other requests are forwarded to the origin unchanged.
CloudFront Functions run at every edge location with sub-millisecond startup and are ~6× cheaper than Lambda@Edge. They are the right tool for stateless, CPU-light request manipulation that requires no network I/O, no large runtimes, and no response body streaming. This filter fits that profile exactly: pure string matching, no external calls.
The trade-off is a restricted runtime — no setTimeout, no fetch, no Node.js built-ins. The function is written deliberately to stay within those constraints.
Copy the body of function.js into the CloudFront Functions editor in the AWS Console (or deploy via AWS CLI / CDK / Terraform). Associate the function with the viewer request event of your distribution.
Important: remove the
export { handler }line before deploying — CloudFront's JS 2.0 runtime does not support ES moduleexportsyntax. That line exists solely so Vitest can import the function during testing.
A Claude Code PreToolUse hook automatically validates function.js against the live cloudfront-js-2.0 runtime before every git push. It uploads the local code to the DEVELOPMENT stage and runs aws cloudfront test-function, blocking the push if any syntax or runtime error is detected.
1. Set your function name
echo "Block_Intrusions" > .cloudfront-function-name
2. Configure the test event
Edit test-event.json to match a representative viewer request for your distribution. The default covers a standard GET with a User-Agent header.
3. AWS credentials
Ensure your shell has credentials with at least these permissions:
{
"Effect": "Allow",
"Action": [
"cloudfront:DescribeFunction",
"cloudfront:UpdateFunction",
"cloudfront:TestFunction"
],
"Resource": "*"
}On each git push Claude Code will:
- Fetch the current ETag via
aws cloudfront describe-function - Upload local
function.jsto the DEVELOPMENT stage viaaws cloudfront update-function - Run
aws cloudfront test-function --stage DEVELOPMENT - Block the push and display the error if the runtime rejects the function
The hook script is at .claude/hooks/cloudfront-pre-push.sh.
Tests are written with Vitest.
Vitest was chosen over Jest for this project because:
- Native ESM support — no Babel transform needed. The function uses
export { handler }which works out of the box with"type": "module"inpackage.json. - Zero config — no
jest.config.js, no transform pipeline to maintain. - Fast — Vitest starts in milliseconds; the full suite runs in under 250 ms.
- Jest-compatible API —
describe,it,expect,it.eachare identical, so the syntax is familiar.
npm test # run once
npm run test:watch # watch mode (re-runs on file save)function.test.js covers all behaviours with 117 tests:
| Suite | What is tested |
|---|---|
| Always-allow paths | /ads.txt; URI trim & lowercase normalisation |
| Security scan blocking | File extensions, scanner folders, sensitive paths |
| Blocked bots | Empty /feed.xml; empty /rss.xml; empty /sitemap.xml; 304 Not Modified on cache headers |
| Bot patterns | 60+ patterns matched case-insensitively |
| Malformed Firefox UA | Mismatched rv: and firefox/ versions blocked |
| Null / empty UA blocking | Missing/empty/whitespace user-agent |
| Pass-through | Normal requests forwarded unchanged |
Each test builds a minimal CloudFront event object ({ request: { uri, headers } }) and asserts on the return value — either the original request object (pass-through) or a synthetic response with statusCode, headers, and body.