An Agent Skill that teaches an agent to put a shared-PIN gate in front of an entire
Next.js App Router site from the proxy or middleware layer: one env var arms it, a self-contained unlock
page sets an HMAC-derived HttpOnly cookie, and the app behind it stays untouched. Unset the variable and the
gate is gone.
Everything dangerous in a gate lives in the two places it touches the outside world: the return path it redirects to, and the cookie it hands out. The rest is a form and a cookie check. Get those two right and one shared PIN is a complete answer for the weeks before a launch, a client or investor preview, or a staging domain: everyone who should see the site can be told the PIN, and nobody else, crawlers included, sees anything but a 401 page. It is not user authentication and not per-route authorization, because a shared PIN cannot be revoked for one person.
This skill was written by the engineer who has shipped this module. The earlier implementation it was audited
against was a temporary gate on a marketing site kept private ahead of its launch. The templates hold the
properties a gate has to hold: an unlock that can only return to the site's own origin, a redirect the browser
follows as a GET so the PIN is never sent twice, a cookie that is opaque to anyone without the server's
secret, comparisons that take the same time whatever the input, a request body that is answered rather than
thrown, and a failed-attempt budget that answers 429. The behaviour contract and the handler suite state each
one; references/provenance.md has the record.
One command, via the skills.sh CLI, which installs the skill into every skills-compatible agent it detects, including Claude Code, Codex CLI and Gemini CLI:
npx skills add timerise-ai/site-pin-gateName the agents instead with -a, for example npx skills add timerise-ai/site-pin-gate -a claude-code -a codex.
Nothing here is Claude-specific: the skill is a plain Agent Skills folder,
SKILL.md plus markdown references with no file that calls a model, so cloning it into an agent's skills
directory is all an install is. For Claude Code:
git clone https://github.com/timerise-ai/site-pin-gate.git ~/.claude/skills/site-pin-gateTo scope it to a single project instead, clone it into that project's .claude/skills/ directory. For another
agent, clone into that agent's skills directory, or symlink the Claude Code copy so one git pull updates
every agent:
mkdir -p ~/.agents/skills
ln -s ~/.claude/skills/site-pin-gate ~/.agents/skills/site-pin-gateUpdate the skill with git pull in its directory. The current release is 0.5.0. See
CHANGELOG.md. The skills index lists the other
Timerise Skills and how to install them all at once.
The skill activates automatically when a task matches its description: hiding a site behind a PIN or password
before launch, locking a preview or staging deployment, adding a coming-soon or access-code page, or auditing
an existing gate in middleware.ts or proxy.ts. Invoke it explicitly with /site-pin-gate in Claude Code,
$site-pin-gate in Codex CLI, or from /skills in Gemini CLI.
A single bare argument is taken as the PIN: /site-pin-gate 1111 arms the gate with that PIN, writes it to
.env.local and nowhere else, and reports what a PIN that short is worth. More than one word is read as a
task description, so /site-pin-gate audit middleware.ts still behaves like a plain invocation. With no PIN
given and nobody to ask, the skill writes no PIN anywhere, not even a development one in .env.local, and
hands over that the site is public until SITE_PIN is set.
uninstall is a reserved word, never a PIN: /site-pin-gate uninstall removes everything the skill added to
the app and nothing else, meaning the wiring in the proxy or middleware, the lib/site-gate directory with
its tests, and the three env vars from .env.local, .env.example and every environment in the platform's
store. The site is public on the next deploy, so the skill says what it will delete and confirms before the
first removal. To lock the site again later, unset SITE_PIN and keep the code instead.
Each host matches a task against the description its own way, so invoke the skill explicitly on a first run
rather than assuming it fired. Only SKILL.md is read up front; the references/ files load on demand, so
the skill stays cheap in context until a topic is actually needed.
| File | Contents |
|---|---|
SKILL.md |
Entry point: architecture diagram, critical facts, hard rules, quick start, and the reference directory |
references/adaptation.md |
The seam contract with the host app: proxy versus middleware, the machine callers, the matcher or a bypass list beside a shared one, strings, a host locale resolver and RTL, styling, cookie name and lifetime (a session cookie), a shared attempt store |
references/module.md |
Config, token derivation, constant-time compare, the return-path sanitiser, the attempt store |
references/handler.md |
The behaviour contract, the gate page, the request handler, the proxy wiring |
references/operations.md |
Env vars per environment, a PIN given at invocation, smoke checks, rotation, kill switch, uninstalling, what stays public, why platform protection breaks webhooks, browser test runs, apps built from an older copy, extensions |
references/testing.md |
The two test files, 41 tests, and how to run them under vitest or bun |
references/provenance.md |
The engineering ledger, in two sources: what the audit of the earlier implementation changed and how the templates verify it, what was kept on purpose, what is new in the skill, and what applying it to a second host taught |
evals/ |
The prompts an operator types after installing (prompts.md) and one file per agent eval: the skill installed into an empty Next.js app, one prompt, no help, then type-checked, built and tested |
The seam is the table at the top of references/adaptation.md, and it is short because the gate keeps no
state beyond one cookie and one counter: there is no domain vocabulary to rename, no tenant scope and no data
access. The attempt store is the one pluggable dependency, in-memory by default with a shared-store adapter
sketched for a hard global cap. The matcher, cookie name, unlock path, strings, palette, locale detection and
logger are the host app's, and so is locale negotiation when the host has its own (resolveLocale, with localeDir for right-to-left languages). A host whose proxy already runs on nearly every path keeps its matcher and skips the gate in code for webhook, cron and feed paths. No template adds a dependency: they import only next/server and Web Crypto.
These travel with the module and are never optional. Each is stated as a hard rule in SKILL.md and covered
by the suite in references/testing.md:
- The return path is resolved against the request origin, never checked with
startsWith('/'). A path that begins with a slash can still leave the site, even after one resolve (/a/..//evil.examplenormalises to//evil.example), so the sanitiser resolves the value, compares origins and checks the result again. Five off-origin path cases are in the suite. - The unlock redirect is a 303. A 307 tells the browser to repeat the request with its method and body, so the PIN would be posted again to the landing page, and once more through any locale redirect. The status is asserted on the success path.
- The cookie is an HMAC keyed by a server-side secret, not the PIN and not an unkeyed hash of it. A cookie an operator can read is a cookie an operator's laptop can leak. A token derived without the secret is rejected in the suite.
- Both comparisons are constant-time over equal-length digests, so neither the PIN check nor the cookie check leaks its answer through how long it took.
- A body that is not a form is answered, never thrown.
request.formData()rejects anything that is not form-encoded, and an unhandled rejection in the proxy is a stack trace instead of a rejected attempt. The 400 path is in the suite. - The matcher is chosen on purpose, and the unlock path sits outside every locale prefix and app route.
Whatever the matcher excludes is public, and so is whatever a bypass list beside a shared matcher names, including the sitemap and OG images unless you say otherwise.
Build assets stay outside it: gating
_next/statichides nothing from a crawler, which never holds the cookie. A colliding unlock path would never render, because the gate answers it before routing.
Everything else is the host app's: cookie name, unlock path, strings, palette, locale detection, attempt store.
| Not this | Use instead |
|---|---|
| Real users with real accounts | The host's auth: Clerk, NextAuth, Supabase Auth. A shared PIN cannot be revoked for one person |
| Per-route or per-tenant authorization | The host's authorization layer; this gate is all-or-nothing by path matcher |
| Locking preview deployments for team members on Vercel, with no webhooks to receive | Vercel Deployment Protection, which does it with no code. It refuses webhooks and other machine callers; the comparison is in references/operations.md |
| Protecting an API consumed by machines | An API key; a cookie and an HTML form are the wrong shape |
Issues and pull requests are welcome here. Pure markdown, with no build step, but the code blocks are checked:
every TypeScript block names its destination on the first line, and the module, handler and test blocks are
written to compile as one project under strict and noUncheckedIndexedAccess and to run under bun test,
41 tests. Claims in this skill are meant to be verifiable: if you change a factual claim, say how you verified
it, whether against the library, the HTTP specification, the URL parser, or a reproduction.
Adding, removing or renaming a file in references/ means updating the quick start and the reference
directory table in SKILL.md, the file table above, and any relative cross-links. Every odd-looking part of
the templates is there for a reason, and references/provenance.md is the ledger that must stay truthful:
read it before simplifying anything, and add an entry for anything you change. Commits follow Conventional
Commits and releases follow STANDARD.md in the
index; CLAUDE.md carries the full editing conventions.
This is one of the Timerise Skills: modules for Next.js App
Router apps written by our own senior engineers from the modules they have shipped, not synthetic, each
published as its own repository and indexed there. They share one layout, so an agent that has read one knows
how to read the next: a SKILL.md entry point, references/ loaded on demand, and a seam contract carrying
the module's non-negotiables.
Built and maintained by Timerise.
MIT. See LICENSE.