freehire / web
AGENTS.md3.5k starsChanged 3 months agoMITView on GitHub
SvelteKit app under web/, consuming the freehire API at /api/v1/*. Same-origin in production; in dev the Vite proxy (web/vite.config.ts) forwards /api to the backend. - Auth is cookie-based via HttpOnly; SameSite=Lax — never a Bearer header or localStorage. The SPA cannot read the token (XSS-safe); the browser attaches it automatically. SameSite=Lax + same-origin is the CSRF defence — no CSRF token. - OAuth buttons render from GET /api/v1/auth/oauth/providers. Callbacks 302 back to the SPA; failures 302 with ?auth_error=oauth, never JSON.…
We split this AGENTS.md into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 92
Mentorship ships behind
users.beta_tester, WHOLE: Every route it owns — the public directory, a mentor's page, and all four cabinet tabs … - Very specific: line 164
No CSP connect-src restriction is set — a comment in web/vite.config.ts records the ingest host for any future connect-src.
≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
SPA conventions
SvelteKit app under web/, consuming the freehire API at /api/v1/*. Same-origin in
production; in dev the Vite proxy (web/vite.config.ts) forwards /api to the backend.
Always true
- Auth is cookie-based via
HttpOnly; SameSite=Lax— never a Bearer header orlocalStorage. The SPA cannot read the token (XSS-safe); the browser attaches it automatically.SameSite=Lax+ same-origin is the CSRF defence — no CSRF token. - OAuth buttons render from
GET /api/v1/auth/oauth/providers. Callbacks 302 back to the SPA; failures 302 with?auth_error=oauth, never JSON. - Four actions need a second proof on top of the session — creating and revoking an API
key, changing the password, deleting the account — and answer
428 recent_auth_requiredwithout one.ConfirmIdentity.svelteis the only surface that asks for it: it decides whether the account confirms by password or by provider,prove()produces the proof (and refuses to send a blank one), andhandleRefusal()words the401/428so no caller keeps its own copy. Inside aDialog, passactive={open}—Dialogrenders its children unconditionally, so an always-active instance fetches providers on pages whose dialog is never opened. The exception is the password-change form: the current password already is the confirmation, so a428there means the session cannot bind a proof at all and the only remedy is signing in again. - A pending action survives the provider round trip in
sessionStorage(recentAuth.ts,ReauthDraft), because confirming leaves the site entirely. Secrets and anti-mistake barriers never go in it: the typed key name is restored, the typed address confirming an account deletion deliberately is not. stagein job tracking mirrors the backend vocabulary (internal/application/userjob/stages.go): preparing/applied/screening/responded/interview/offer/accepted/rejected/withdrawn/expired.- A view is recorded silently when a signed-in user opens a job — failure is swallowed and must not break the page.
MatchSummary.svelteis the compact sidebar summary (overall % + verdict + top gap) linking to the full analysis in the/tailor/[slug]workspace — it never computes inline.- The profile headshot (
HeadshotField.svelte, inProfileFormon the profile's main tab) is one image per member, reused by every CV.GET /api/v1/me/photois metadata — the bytes are the/imagesub-resource, and the URL carries?v=<uploaded_at>because it is otherwise stable. The control renders nothing whenenabledis false (object storage unconfigured). ResumeStructuredView.svelteis read-only; the backend serves the structured CV fromGET /api/v1/me/resume(structuredfield, null when absent/stale/unconfigured).- Environment variables reach the app only through
src/env.ts. SvelteKit 3 exposes nothing undeclared — not even through the deprecated$env/*modules — so a variable missing there isundefinedat runtime however the host sets it, and nothing reports it. Declare it there and read it from$app/env/public/$app/env/private. - The public origin (
page.url.origin, behind every canonical URL) is baked in at BUILD time fromORIGIN—paths.origininvite.config.ts. adapter-node no longer readsORIGINat runtime, so setting it only on the running server does nothing. - Sentry is gated on
PUBLIC_SENTRY_DSN(+PUBLIC_SENTRY_ENVIRONMENT); source-map upload only whenSENTRY_AUTH_TOKEN/SENTRY_ORG/SENTRY_PROJECTare set (build succeeds without them — and, because an upload failure is swallowed, it succeeds with them and a dead token too).scripts/sentry-credential-check.mjsis what actually asks, and its header is the canonical account of why the build cannot;freehire-ops' scripts/host2/release.shruns it before the build and refuses the release on a rejected or half-written credential. - PostHog is gated on
PUBLIC_POSTHOG_KEY(inert without it — no init, no events); ingestion goes through the same-origin/ingestreverse proxy (nginx →eu.i.posthog.com), overridable viaPUBLIC_POSTHOG_HOST. Injected byfreehire-ops, never committed, unset in dev. At boot the start is deferred torequestIdleCallback(3s timeout) so gtag.js and the PostHog SDK stop competing with first paint; calls made before the SDK resolves are queued in#lib/analytics.jsand replayed in order, which is what keeps the initialafterNavigatepageview and — load-bearing — the/my/**stopSessionRecording()from being dropped. Accept-in-the-banner still starts them immediately. hooks.server.tsnarrows theLinkheader to the stylesheet and the two entry modules. SvelteKit's default preloads a route's whole module graph, which here is ~85 sub-1.5KB chunks — ~90modulepreloadfetches racing the render-blocking CSS. Removing them cut mobile LCP from 8.5s to 4.4s on/and 7.8s to 4.7s on a job page (Lighthouse, Slow 4G, same rig). Notesequence()semantics: the FIRSTpreloadwins, later ones are ignored.- The header's GitHub star count comes from our own
/github-stars, notapi.github.com. Called from the browser it spent the visitor's share of GitHub's 60/hour-per-IP cap and returned 403 from any shared address;#lib/server/github.jsholds one process-wide hourly cache, shared with/open.stars: nullis a valid answer — the badge drops the number, never errors. Unauthenticated even server-side, so a busy IP still degrades to null. - There is no PWA any more (removed 2026-10-18). It bought an icon and a window without an
address bar: nothing was cached for offline use (every listing is live or personal), the
hashed assets already carry a year-long immutable cache, and there is no web push. What it
cost was a stale precached shell serving deleted chunks as the 500 page, and from 2026-09-08
a precache whose every URL 404ed.
web/static/sw.jsis a kill switch for browsers that still have the old worker registered — it clears their caches and unregisters itself; nothing registers it otherwise. Do not bring a service worker back without a reason it serves.
Worth knowing
- Job-fit analysis: the standalone
/match/[slug]/page is gone — its+page.server.tsis only a 308 redirect to/tailor/[slug], and the Tailor workspace is the sole surface for viewing and triggering the analysis. The analysis UI isMatchAnalysisFull.svelte(stepper + thinking panel + progressive sections over anEventSource), embedded in the workspace'sArtifactPaneland inJobDrawer.svelte. The pure SSE reducerreduceMatchEventlives inweb/src/lib/matchAnalysis.ts(unit-tested). ReferralBlockimports its modal on the click that opens it. The block is small; theRequestReferralModalbehind it pulls in Dialog, FormField and the request flow, and it sat in the module graph of every job and company page while only the few visitors who actually ask for a referral open it. The block itself stays server-rendered — deferring it too would push the description down after hydration and trade the CLS the page currently has at zero.- Mentorship ships behind
users.beta_tester, WHOLE. Every route it owns — the public directory, a mentor's page, and all four cabinet tabs — answers 404 to anybody without the flag, andMentorBlockrenders nothing. The predicate is#lib/server/mentorshipGate.js. Ending the beta is three edits, not one: that file,inBetainMentorBlock.svelte, andbetaOnlyon/my/mentorshipinaccountNav.ts— a door, an offer of the door, and a menu entry, deliberately separate. Grep forbeta_testerandbetaOnly; those three are all of it. Gating only the cabinet was considered and rejected: it would leave a visitor able to BOOK from a public profile and then unable to find the session again or cancel it, and the hour a mentor is holding is real. 404 rather than 403 — while the feature is unreleased the honest answer is that the page is not there, and a 403 advertises it to every crawler that finds the URL. - Mentorship (
/mentors,/mentors/[slug],/my/mentorship): the arithmetic lives inweb/src/lib/mentorship.ts, the same splitmatchAnalysis.tsholds — pure,$app-free, unit-tested. Nothing in the slot path constructs aDate. A slot arrives withlocal_startalready resolved in the viewer's zone, so parsing it re-interprets it against the browser's: Tokyo's 16th at 01:00 is still the 15th in UTC, and the slot files under the day before the one offered. The month grid is integer arithmetic for the same reason. A BOOKING is the opposite case — it carries only instants, so the zone is applied here. Every slot renders itsutc_offset, because on the autumn transition two slots share a wall clock and differ only there. The profile page is server-rendered and its SLOTS are not: the server cannot know the viewer's zone, and rendering them in UTC then swapping after hydration shows one set of hours and silently replaces it. Booker state is?month/?date/?slot, held in the component and only MIRRORED to the address bar —page.urllags a shallowreplaceState(seeurlSynced.svelte.ts), so deriving from it moves the URL and not the screen. That URL is also what survives the sign-in bounce, which is why the sign-in link is built fromlocation.searchrather thanpromptSignIn(). With no?monththe calendar opens on the first month that has an open hour: its first read searches 62 days forward from today's UTC boundary (openingSlotWindow, the endpoint's maximum window, shared for caching). Calendar-month boundaries can miss valid slots: 30 days from late January can reach March. MentorBlockasks from the browser, not fromload. It answers "does this company have a mentor?" out of the DIRECTORY narrowed to that company — never a second endpoint, which would be a second copy of the publication predicate. It runs client-side because the job page is the busiest surface here and roughly three quarters of this host's traffic is crawlers: a server-side call would spend an API request on every bot fetch to answer a question no bot acts on. It renders nothing when there is no mentor, so callers need no gate of their own.color-schemeis declared inapp.css, light by default and dark under.dark. The menu a native<select>opens is drawn by the OS, and without this it follows the OS setting rather than the site's — a visitor whose machine is dark got a black dropdown over a white page. There is no way to style a native menu;color-schemeis the whole API, and it also covers scrollbars, form-control focus rings and date pickers.- Filters: the companies FilterModal uses
COMPANY_FACETSfromweb/src/lib/facets.ts, including a "Remote hiring" pill that reuses the sharedREGIONvocabulary for theremote_regionsoverlap facet. AssistantChathas three hosts:/my/assistant,/tailor/[slug], and the profile page's Experience tab (ExperienceAssistantPanel.svelte). It needs a flex parent with a bounded height, and it boots a session on mount — so never mount it before the user asks for one, or every visitor mints an empty conversation.kickoffis read once, into a deliberately non-reactivearrival, and spent after the first send: setting the prop on a live chat does nothing, so aiming it at a new subject means remounting it (the Experience panel keys on a launch token). Unmounting cancels a streaming turn, which is why closing that panel hides it instead. The opening text for theprofileinterview comes fromprofileKickoffin#lib/assistant/presets— shared with the?preset=profile&atoms=…URL entry so the two cannot drift. That message names achievements by id and asks the agent to read them first; it deliberately does not name the tool, because it is recorded as the candidate's own message.- The Experience panel is docked to the VIEWPORT, not laid out in the tab. It is owned
by
ExperienceBankViewbut positionedfixed, andmy/+layout.svelteyields to it throughdockOffset()in#lib/assistantDock.svelte.ts— a module store, because the panel opens inside a page and the offset is applied by the layout across a route boundary. It was a column inside the content area and spent the bank's width; the shell now steps aside instead. The layout also collapses the section nav to its icon rail while the dock is open, which is what keeps the bank at least as wide as it was — that override is never persisted, and an explicit toggle cancels the restore. Docking starts atDOCKED_QUERY(85rem); below it the panel covers the bank as a modal overlay and claims no offset. Both numbers are derived inassistantDock.svelte.ts— change one and redo the arithmetic. - The
TopBarissticky top-0 z-40andh-14. Anything else that pins to the top of the page needstop-14and a z-index below 40, or it pins behind the header and is invisible exactly when it is needed.
Limitations
- No CSP
connect-srcrestriction is set — a comment inweb/vite.config.tsrecords the ingest host for any futureconnect-src. - OAuth identity unlinking/management UI is not implemented (backend seam mirrored on the frontend).
easymde(theNoteEditor.sveltemarkdown field for private job-tracking notes) carries a known ReDoS in its bundledcodemirror@5.x, with no safe upgrade or override available — seeweb/.snykfor the accepted-risk reasoning and its revisit date.
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository4 other files it gives its agents.
- AGENTS.mdAGENTS.md
- design-system/AGENTS.mdAGENTS.md
- extension/AGENTS.mdAGENTS.md
- .cursor/rules/commit-checks.mdcCursor rule
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

