Meta-rules
Most tsforge rules lint .ts files through ESLint. Meta-rules are different: they inspect project files that normal lint does not cover.
Think of them as housekeeping checks. Is package.json pinned correctly? Is tsconfig.json strict? Does every logic module have a colocated test file? Are GitHub Actions pinned with explicit permissions?
They run in the same gate pass as ESLint. A meta-rule failure blocks the task the same way a type error would.
Tune meta-rule severity in tsforge.config.json the same way as ESLint rules. The strict profile elevates key supply-chain and CI checks to error by default.
What they look at
Section titled “What they look at”| Area | Example checks |
|---|---|
package.json | exact version pins, lockfile present, packageManager field, no git/tarball deps |
tsconfig.json | strict mode on, recommended flags, include paths exist |
| Source text | no @ts-ignore, no eslint-disable comments |
| CI workflows | pinned actions/runners, timeouts, explicit permissions, no unsafe pull_request_target |
| Dockerfiles | pinned base image, non-root USER, no secret literals in ENV/ARG |
| Next.js config | no wildcard image hostnames, instrumentation present |
| Tests | service/util files have a matching *.test.ts sibling |
| Drizzle (when pack active) | migrations checked in, no drizzle-kit push in CI |
All 31 rules
Section titled “All 31 rules”Supply chain
Section titled “Supply chain”| ID | Checks |
|---|---|
package-exact-deps | no ^/~ ranges in dependencies |
no-overlapping-libs | forbid redundant HTTP client libraries |
fastify-security-plugins | helmet/cors/rate-limit when using Fastify |
lockfile-required | exactly one lockfile for the detected package manager |
single-package-manager | no mixed lockfiles |
package-manager-field-required | packageManager in package.json |
no-git-or-tarball-dependencies | warn on git+ / HTTP tarball URLs |
no-undeclared-dependencies | every imported package is declared in package.json (no relying on hoisting) |
dependency-overrides-require-comment | overrides/resolutions need an adjacent comment |
production-must-not-use-drizzle-push | no drizzle-kit push in scripts/CI (Drizzle projects) |
migrations-must-be-checked-in | drizzle/ or migrations/ folder exists (Drizzle projects) |
Source text
Section titled “Source text”| ID | Checks |
|---|---|
no-eslint-disable-comments | no inline disables |
no-ts-suppressions | no @ts-ignore / @ts-expect-error / @ts-nocheck |
Both scan every hand-written .ts/.tsx under src/, tests/, and scripts/. Generated *.gen.ts files are skipped. Codegen output (e.g. TanStack’s route tree) legitimately ships with /* eslint-disable */ + @ts-nocheck, and the model can’t write *.gen.ts anyway (it’s vendored/read-only), so the ban stays airtight everywhere the model actually writes.
Config
Section titled “Config”| ID | Checks |
|---|---|
tsconfig-paths-exist | include paths exist on disk |
tsconfig-strict | strict mode on |
tsconfig-recommended-flags | useUnknownInCatchVariables, erasableSyntaxOnly, exactOptionalPropertyTypes, and related flags |
next-proxy-over-middleware | migrate middleware.ts → proxy.ts (Next.js 16) |
next-instrumentation-present | instrumentation.ts with OpenTelemetry (Next.js apps) |
next-image-remote-patterns-no-wildcards | no ** hostnames in remotePatterns |
Testing
Section titled “Testing”| ID | Checks |
|---|---|
test-sibling-required | a logic file (one exporting a function/class) the agent changes has a test: co-located *.test.ts or mirrored under tests/. Error when TDD mode is on (the default), warn when off. Scoped to changed files, so it never blocks on pre-existing untested code. |
| ID | Checks |
|---|---|
workflow-actions-pinned | pinned action refs (tag or SHA) |
workflow-runner-pinned | no ubuntu-latest |
workflow-timeout-required | job timeouts set |
workflow-permissions-explicit | every workflow declares permissions: |
workflow-permissions-least-privilege | warn on broad contents: write / id-token: write |
no-pull-request-target-untrusted-checkout | unsafe pull_request_target + PR head checkout |
no-github-context-in-shell | ${{ github.event in run: without env indirection |
Structure (cross-file)
Section titled “Structure (cross-file)”Per-file ESLint can’t see the module graph; this builds it from the project’s own relative imports.
| ID | Checks |
|---|---|
no-circular-imports | no import cycle (A → B → A). They cause partial-initialization bugs and defeat tree-shaking |
Container
Section titled “Container”Active whenever a Dockerfile (Dockerfile, Dockerfile.*, or *.Dockerfile) exists at the project root or one level down. No-op otherwise.
| ID | Checks |
|---|---|
dockerfile-base-image-pinned | every FROM pins an explicit non-latest tag (or @sha256: digest); build-stage refs and scratch are exempt |
dockerfile-non-root-user | a non-root USER is declared so the container does not run as root |
dockerfile-no-secrets-in-env-arg | no secret-looking ENV/ARG literal (*_KEY, *_TOKEN, *_SECRET, *_PASSWORD): inject at runtime |
Severity
Section titled “Severity”| Level | Effect |
|---|---|
error | Gate fails. Model must fix it. |
warn | Reported but gate can still pass. |
off | Silenced via tsforge.config.json. |
Full list with descriptions: Rule catalog (meta-rules section at the bottom).