Fastify 5
Fastify 5.12.5+ services and plugins without Fastify 3/4 APIs, with explicit encapsulation, one completion model per hook and handler, trusted schemas, bounded inputs and package-manager-owned dependencies. Rule IDs F5-V/P/H/S/C/T/X; NEED TO blocks.
Set this profile
ccpp cpo profile set fastify-5Inside Claude Code, prefix the command with ! to run it in the session. Downloading a profile needs a Builder or Team plan; listing them does not.
Policies
110 policies, grouped by what they guard.
Untrusted input 5
Requests, downloads and tool output are validated before they reach SQL, a shell, eval or a deserializer.
f5-c06-request-derived-pathsF5-C06 · File paths and authorities built from request input are contained (no traversal, no Host-header URLs)
f5-c06-request-derived-redirectF5-C06 · Redirect targets never come straight from request input (open redirect): validate against an allow-list or keep them relative
f5-s01-unvalidated-inputF5-S01 · A route that consumes body, query or params declares a runtime contract for them (schema or an explicit validator)
f5-s05-untrusted-schemaF5-S05 · Schemas compiled by Fastify or Ajv never come from request data
f5-s15-cast-as-validationF5-S15 · TypeScript casts of request.body/query/params are not validation: a route without a schema does not assert their type
Network & TLS 1
Outbound data, verified certificates, destinations taken from requests and services exposed to others.
f5-c05-trust-all-proxiesF5-C05 · The user confirms trusting every proxy (trustProxy: true): X-Forwarded-* then decides request.ip, host and protocol
Weakened checks 3
Suppressions, disabled lint or CI steps and relaxed strictness need a reason or the user's approval.
f5-t06-noop-scriptsF5-T06 · package.json test, lint and typecheck scripts keep running real checks (no echo/exit 0, no --passWithNoTests added)
f5-t06-weakened-checksF5-T06 · Checks are not weakened to make a change pass: no blanket ts-ignore/eslint-disable, no focused tests, no no-op test or lint scripts
f5-t07-warning-suppressionF5-T07 · Fastify warnings (FSTDEP, FSTWRN, FSTSEC) are fixed at their source, not suppressed process-wide
Error handling 3
Failures are propagated or handled, never swallowed or turned into fake success.
f5-c15-error-handler-overrideF5-C15 · One intentional error handler per scope: no second setErrorHandler on the same instance (FSTWRN004)
f5-c16-error-handler-contractF5-C16 · Custom error handlers keep error statuses and never expose stacks or internal error data
f5-c17-404-in-error-handlerF5-C17 · Not-found responses are customized with setNotFoundHandler, not by testing 404 in setErrorHandler
Async & concurrency 1
Owned tasks, awaited promises, preserved cancellation and no blocking on event loops.
f5-c04-cancellationF5-C04 · Under handlerTimeout, outbound calls of a handler follow request.signal (the timeout does not stop work by itself)
Performance & resource bounds 2
Timeouts, bounded retries and fan-out, reused clients, closed resources, no N+1 queries.
f5-c02-route-body-limitF5-C02 · Route and parser bodyLimit overrides stay finite (no Infinity / MAX_SAFE_INTEGER)
f5-s06-compile-on-hot-pathF5-S06 · Validators, serializers and Ajv instances are built at boot, not per request inside handlers or hooks
Architecture & boundaries 5
Declared layers and dependency directions, no import cycles, small modules, explicit dependencies.
f5-p12-import-side-effectsF5-P12 · A module that exports the app or a plugin does not listen or install process handlers at import (startup owns sockets and signals)
f5-p13-transport-objects-in-servicesF5-P13 · Business operations receive their inputs, not Fastify's request or reply objects
f5-p14-repeated-cross-cuttingF5-P14 · Authentication and authorization checks repeated across handlers of a module move into one scoped hook
f5-p15-import-cycleF5-P15 · A new import does not close a cycle of value imports between modules (up to three hops)
f5-p16-handler-budgetF5-P16 · Route handlers and hooks stay within the project's line budget (maxHandlerLines; off by default)
HTTP & APIs 13
Request and response contracts, schemas, status codes, error bodies and published API surfaces.
f5-c08-manual-body-accumulationF5-C08 · Bodies are parsed by Fastify's parsers (built-in or addContentTypeParser with parseAs and bodyLimit), not by reading the raw request stream
f5-c09-content-type-matchingF5-C09/C10 · Content-type parsers match precise media types (anchored regexes, no catch-all) and keep the built-in ones
f5-c18-version-varyF5-C18 · Routes constrained by version also send Vary: Accept-Version (caches must keep variants apart)
f5-c20-swagger-legacy-optionsF5-C20 · @fastify/swagger is configured with current options: UI options belong to @fastify/swagger-ui
f5-c20-swagger-orderF5-C20 · @fastify/swagger is registered before the routes it documents, and the spec is generated after ready()
f5-s02-response-contractF5-S02 · A route with a request schema that returns structured data also declares its response schema
f5-s04-duplicate-schema-idF5-S04 · Shared schemas registered with addSchema have unique $id values in a scope
f5-s07-async-validationF5-S07 · Schema validation stays synchronous: no $async schemas or async Ajv keywords for database or network lookups
f5-s08-async-validator-envelopeF5-S08 · An asynchronous custom validator rejects on failure instead of resolving an { error } envelope
f5-s10-attach-validationF5-S10 · A route with attachValidation: true branches on request.validationError before using the input
f5-s14-header-schema-refF5-S14 · Header schemas are inline (case-normalized), not external $ref (FSTSEC002)
f5-s16-type-provider-scopeF5-S16 · A plugin of a project using a type provider keeps it: provider-specific plugin types or withTypeProvider, not a plain FastifyPluginAsync
f5-s17-legacy-type-providerF5-S17 · Custom type providers implement Fastify 5's validator/serializer members, not the v4 input/output interface
Framework conventions 27
Routing, server and client boundaries, hooks and plugins the way the framework expects them.
f5-h01-mixed-completionF5-H01 · Plugins and hooks use one completion model: async functions take no done/next callback and never call it
f5-h02-hook-namesF5-H02 · addHook receives a hook Fastify 5 knows; beforeHandler and other legacy names are not hooks
f5-h03-late-registrationF5-H03 · onReady, onListen and onClose hooks do not register routes, hooks, plugins or decorators (the instance has booted)
f5-h03-onroute-after-routesF5-H03/H17 · onRoute observers are added before the routes they must see, and an onRoute hook adding routes guards against itself
f5-h04-body-before-parsingF5-H04 · request.body is read only after parsing (preValidation or later), never in onRequest or preParsing
f5-h06-preparsing-streamF5-H06 · A preParsing hook that replaces the payload returns a stream (not a string, Buffer or parsed object)
f5-h07-early-response-continuesF5-H07 · A hook or handler that answers stops the chain: no done() after reply.send, and a deferred send returns reply
f5-h08-send-and-returnF5-H08 · An async handler completes once: it returns the payload or sends and returns reply, never both
f5-h09-async-fallthroughF5-H09 · Every path of an async handler returns a payload, sends, throws or hijacks (no silent undefined fall-through)
f5-h10-onerror-responseF5-H10 · onError hooks observe errors: no reply.send and no error passed to done (setErrorHandler maps responses)
f5-h11-onsend-payloadF5-H11 · onSend replaces the payload with a serialized value (string, Buffer, stream or null), never an object
f5-h12-response-after-completionF5-H12 · onResponse, onTimeout and onRequestAbort hooks never try to send or modify the response
f5-h13-raw-response-without-hijackF5-H13 · Raw response writes (reply.raw.write/end/writeHead) happen only after reply.hijack(), in an isolated adapter
f5-h14-resources-without-oncloseF5-H14 · A plugin that opens pools, clients, timers or listeners releases them in an onClose (or preClose) hook
f5-h15-unhandled-start-stopF5-H15 · listen/ready failures are handled and close completes before the process exits
f5-h16-readiness-in-onlistenF5-H16 · Readiness-critical work (connections, migrations, warm-up) runs in plugin boot or onReady, not in onListen
f5-p03-plugin-metadataF5-P03 · A fastify-plugin that provides decorators declares its name, and a published plugin declares the Fastify range it supports
f5-p05-routes-in-fastify-pluginF5-P04/P05 · Routes are not registered directly inside a fastify-plugin wrapper (it breaks encapsulation and register prefixes do not apply)
f5-p07-detached-decorationF5-P07 · Decorations happen during plugin boot (awaited), never later from a .then, timer or event callback
f5-p09-arrow-thisF5-P09 · Handlers, hooks and decorators that use Fastify's this are regular functions, not arrow functions
f5-p10-duplicate-decoratorsF5-P10 · One decorator name is defined once per scope: no duplicate decorate calls, no hasDecorator guards hiding a second implementation
f5-p17-dirname-in-esmF5-P17 · ES modules locate plugin and route directories with import.meta.dirname, not the CommonJS __dirname/__filename
f5-x09-generator-hintF5-X09 · Starting a new Fastify app or plugin by hand gets a hint about the official generators (once per session)
f5-x10-generator-overwriteF5-X10 · The user confirms a Fastify generator run that integrates into or targets an existing, non-empty directory
f5-x12-handrolled-substitutesF5-X12 · Fastify code uses the maintained native plugins instead of Express middleware or hand-written CORS, body parsing and security headers
Versions & deprecated APIs 20
Code matches the installed version, without removed, renamed or deprecated APIs.
f5-v01-installed-versionF5-V01/V02/X05 · At session start, Claude learns the installed Fastify version, its bundled docs and plugins that do not match Fastify 5
f5-v04-listen-optionsF5-V04 · listen takes an options object ({ port, host }): the positional port/host signature was removed
f5-v05-full-json-schemaF5-V05 · Route schemas given to the default compilers are full JSON Schemas (no v4 shorthand property maps for body, querystring, params, headers or responses)
f5-v06-removed-server-optionsF5-V06/V07/V12/C05 · Fastify factory options hold no option Fastify 5 removed or rejects: jsonShortHand, versioning, a logger instance in logger, hop-count trustProxy
f5-v08-removed-request-reply-membersF5-V08/V11 · Route metadata comes from routeOptions, the socket from request.socket and timing from reply.elapsedTime (request.context, routerPath, connection, getResponseTime… were removed)
f5-v09-redirect-signatureF5-V09 · reply.redirect takes the destination first and the status second
f5-v10-reply-sent-assignmentF5-V10 · reply.sent is read-only: the response is taken over with reply.hijack()
f5-v12-route-version-optionF5-V12 · Route versions are constraints (constraints: { version }), not the removed version route option
f5-v13-params-prototypeF5-V13 · request.params has no prototype: no params.hasOwnProperty/toString, use Object.hasOwn
f5-v14-trailer-completionF5-V14 · reply.trailer producers are async or take the done callback (direct return was removed)
f5-v15-head-after-getF5-V15 · A custom HEAD route is registered before the GET of the same URL, or that GET sets exposeHeadRoutes: false
f5-v16-has-route-definitionF5-V16 · hasRoute receives a route definition (method + pattern), not a concrete request URL
f5-v17-unsupported-methodsF5-V17 · Routes use HTTP methods Fastify 5 supports, or register the WebDAV ones with addHttpMethod first
f5-v18-add-existing-methodF5-V18 · addHttpMethod for a method Fastify already supports passes overrideExisting: true deliberately (FSTDEP025)
f5-v19-hostname-portF5-V19 · request.hostname has no port since Fastify 5: the authority is request.host, the port request.port
f5-v20-default-routeF5-V20 · No setDefaultRoute/getDefaultRoute (removed): setNotFoundHandler, or routerOptions.defaultRoute for low-level routing
f5-v21-server-option-choicesF5-V21/V22/V25/C02/C03/C07/C14/C19/S11/S12 · Factory options use routerOptions and logController, keep safe parsing and bounded inputs, and make Ajv and request-ID choices deliberate
f5-v23-middleware-adapterF5-V23 · fastify.use needs a middleware adapter (@fastify/middie or @fastify/express) installed and registered; native hooks and plugins come first
f5-v24-middleware-raw-objectsF5-V24 · Middleware under @fastify/middie gets raw Node req/res: no Fastify or Express reply methods there
f5-v25-wire-formatF5-V25 · New date-time/time formats are checked against Fastify 5's timezone requirement (hint)
Configuration 1
Settings read once at a boundary, validated, with units and without unsafe defaults.
f5-c01-env-in-route-modulesF5-C01 · Route and plugin modules receive configuration through options or a decorator; process.env is read at the startup/config boundary
Logging & observability 2
Structured logs, bounded metric labels, no debug prints left in production code.
f5-c11-console-loggingF5-C11 · Request work logs through request.log / fastify.log (structured, correlated), not console
f5-c12-whole-object-loggingF5-C12 · Logs do not dump whole requests, headers, bodies or queries (secrets reach the log store)
Dependencies & toolchain 19
Packages change through the package manager, lockfiles stay consistent and the project's tools are used.
f5-v02-install-versionsF5-V02 · Fastify is installed within 5.x at or above the security floor: no 3/4/6 specs, prerelease tags or ranges that cannot reach the floor
f5-v02-manifest-rangeF5-V02 · package.json keeps fastify on a 5.x range that can resolve at or above the security floor
f5-v03-node-runtimeF5-V03 · A declared Node.js runtime of a Fastify project meets Fastify 5's minimum (Node 20): engines, .nvmrc, Dockerfile, CI matrices
f5-x01-package-managerF5-X01 · Dependencies are installed with the project's package manager (packageManager field, lockfile)
f5-x01-second-lockfileF5-X01 · No second lockfile next to another package manager's lockfile
f5-x02-hand-written-manifestF5-X02 · A new package.json without dependencies is initialized with the package manager
f5-x02-new-manifestF5-X02/X03 · A new package.json comes from the package-manager initializer (or a generator), and gets its dependencies from the package manager
f5-x03-manual-dependency-editF5-X03 · Dependencies change through the package manager (add/remove/update), never by editing package.json by hand
f5-x03-package-manager-callRecords the tool calls that run a package manager (they may rewrite package.json and lockfiles; F5-X03/X05/X06)
f5-x04-version-lookupsF5-X04 · Registry version lookups are not a step before installing
f5-x04-version-pinsF5-X04 · Packages are installed by name: no guessed exact version, no --save-exact without a reason
f5-x05-forced-installsF5-X05/X06 · Dependency conflicts are repaired, not masked with --force, --legacy-peer-deps or broad overrides
f5-x05-install-pluginsF5-X05 · Installs use the maintained @fastify/* package names and plugin majors compatible with Fastify 5
f5-x05-installed-compatF5-V02/X05 · After a package-manager run, the Fastify and plugins it touched resolve to Fastify-5-compatible releases
f5-x05-plugin-fastify-rangeF5-X05 · A fastify-plugin wrapper declares a Fastify range that includes 5 (a 3.x/4.x range fails at boot)
f5-x06-hand-edited-lockfileF5-X06 · Lockfiles are written by the package manager only (no hand edit, no script rewriting them)
f5-x06-stale-lockfileF5-X06 · A turn that changes package.json dependencies also updates the lockfile next to it (once per stop)
f5-x07-ci-frozen-installF5-X07 · CI and image builds of a project with a lockfile install from it (npm ci, frozen/immutable installs)
f5-x08-pkg-set-hintF5-X08 · Hand edits of package.json metadata or scripts get a once-per-session hint about npm pkg set/delete
Tests 2
Tests assert behavior, stay deterministic and are not removed or skipped silently.
f5-t01-mocked-frameworkF5-T01 · Route tests run real Fastify registration through inject, not a mocked fastify module or hand-made reply objects
f5-t03-unclosed-test-appF5-T03 · A test that injects into or listens on a Fastify instance closes it (teardown hook or finally), even when the test fails
Completion evidence 6
A turn ends with its checks run and passing; claims of success need proof after the last edit.
f5-t06-check-callRecords which checks (lint, typecheck, build, test) a shell call runs, from the parsed commands (F5-T06 ledger)
f5-t06-completion-claimsF5-T06 · A final message claiming passing lint, types, build or tests needs a successful run after Claude's last edit
f5-t06-evidence-ledgerF5-T06 · Records, per session, Claude's edits (file tools and shell writes) and the lint, typecheck, build and test runs that succeeded after them
f5-t06-new-type-errorsF5-T06/S15 · A turn adds no TypeScript errors to the changed files or their importers (language server, once per stop)
f5-t06-required-checksF5-T06 · A turn that changed Fastify code ends only after the configured lint, typecheck and tests succeeded on the final files (once per stop)
f5-turn-baselineRecords the work tree when a prompt starts: the baseline of the Fastify end-of-turn checks (F5-X06, F5-T06)