Frames: what they are and how to select them
On this page
Public reference for operators picking up nimblegate for the first time. Covers: what a frame is, the canonical taxonomy, how to select frames in your project, starter kits, custom user kits, and how to migrate legacy configs.
Related docs:
- Frame reference: per-frame list with categories and tiers
- Writing your own frames: how to write a new frame
- pattern catalog: the shared pattern catalog frames build on
What a frame is#
A frame is a single named check that nimblegate runs at scan time. When the check matches something in the pushed code, the gateway emits a finding, a structured record naming the frame, the file and line, and a one-sentence message. What the gateway does with that finding (block the push, allow and record it, or allow it silently) is decided by the finding’s severity, which the operator can tune per-repo on the Policy page.
Frames are gated by project config: a frame is active if and only if its ID
appears in the [frames] enabled list in appframes.toml. The
dashboard and CLI let you select frames individually, or apply a starter kit
that pre-ticks a curated set.
Each frame ships as a single markdown file. The frontmatter declares the metadata; the body is prose explaining what the frame catches, why, and how to fix the kind of finding it produces.
Frame anatomy#
The frontmatter fields for a stdlib frame:
name: no-hardcoded-credentials # unique short ID within its category
category: security # primary taxonomy category (one of 12 canonical)
subcategory: credentials # bucket within the category
platform: [] # optional - cross-lists under Platform > <value>
framework: [] # optional - cross-lists under Framework > <value>
severity: BLOCK # BLOCK | WARN | INFO (operator-tunable)
tier: 1 # 1–6; used for time-prevented hours math only
tags: [secrets, content-scan] # free-form search/filter chips
triggers: [pre-commit, cli] # where the frame fires
applies-to:
files: ["**/*"]
The markdown body follows the frontmatter and explains the frame in plain English.
Frame files live at internal/stdlib/frames/<legacy-dir>/<name>.md. The
directory reflects the historical organisation; the category: field in
frontmatter is the logical placement used by the dashboard browse tree and
taxonomy validation.
Canonical category list#
Every frame declares exactly one category. The loader validates it against
this set. The dashboard hides categories with zero frames, so Platform and
Framework don’t appear as categories today - no built-in frame declares
either as its category. Platform still shows up in the browse tree, populated
through the platform: cross-listing array described below.
Note that these are the category: frontmatter values, which are deliberately
not the directory names that form frame IDs: git-safety/folder-branch-lock
declares category: git.
| # | Category | What it covers | Subcategories |
|---|---|---|---|
| 1 | security |
Credentials, XSS / content safety, security headers, transport, invisible-payload attacks (Trojan Source / tag-char / zero-width / homoglyph) | credentials, content-safety, headers, transport, invisible-payload, identifier-confusable |
| 2 | network |
Proxy / reverse-proxy configs, routing, DNS resolution | proxy-config, routing |
| 3 | filesystem |
Destructive paths, mount points, file ops | destructive-paths |
| 4 | git |
Branch discipline, history integrity, push-gate integrity, LFS redirection | branch-discipline, history-integrity, gate-integrity, lfs-redirection |
| 5 | commands |
Shell safety, trusted execution, package management | trusted-execution, package-management |
| 6 | app-correctness |
Env config, data fetching, module loading, routing, build integrity (skipped tests, CI that ignores failures, linters switched off) | env-config, data-fetching, module-loading, routing, build-integrity |
| 7 | database |
Migrations, schema drift | migrations, schema-drift |
| 8 | web |
HTML, SEO, a11y, markup validity | html, seo, a11y, markup-validity |
| 9 | documentation |
Markdown, doc drift, TODO discipline, branch consistency | markdown, doc-drift, todo-discipline, branch-consistency |
| 10 | encoding |
BOM, smart quotes, YAML tabs, line endings, mixed indent, en-dash flags, non-printable controls, zero-width in prose | byte-order-mark, smart-quotes, yaml, line-endings, indent, dash-substitution, control-chars, invisible-chars |
| 11 | platform |
Hosting/runtime platforms (Cloudflare, CF Pages): reserved as a category, currently unused - platform grouping comes from the platform: array instead |
cloudflare, cf-pages |
| 12 | framework |
Application frameworks: reserved, currently unused | none |
Subcategory is free-form within a category. New frames may introduce new subcategories without prior registration; the loader only requires the field is non-empty.
Cross-listing via platform: and framework:#
A frame can declare platform: and/or framework: arrays. When it does, the
dashboard browse tree shows that frame under:
- Its primary
category > subcategorylocation - For each value in
platform:: underPlatform > <value> - For each value in
framework:: underFramework > <value>
All paths share a single tick state: toggling a checkbox anywhere updates every browse path that contains the same frame simultaneously. There is no separate “Platform copy” of a frame; it is the same frame ID appearing in multiple places.
Example: security/cf-pages-headers-baseline has category: security,
subcategory: headers, platform: [cloudflare, cf-pages]. It appears under
Security > Headers, Platform > Cloudflare, and Platform > CF Pages. Ticking it
once from any location enables it.
Selecting frames in your project#
Frames are selected by listing their IDs in appframes.toml:
[frames]
enabled = [
"git-safety/folder-branch-lock",
"security/no-hardcoded-credentials",
"security/no-private-keys-in-repo",
# ...
]
[ui]
applied_kits = ["core"]
Rules for the enabled list:
- Flat IDs only. Each entry is
<dir>/<name>exactly as shown in the frame catalog. - No
@-prefixes. The@group-namesyntax was removed in v0.1.0. See the migration section below. - No wildcards.
security/*is not valid. Tick frames individually or apply a kit.
A frame is active if and only if its ID appears in this list. The engine reads it as-is: no expansion, no inheritance.
Starter kits#
A starter kit is a named, pre-defined set of frame IDs. Applying a kit
writes each of its frame IDs into [frames] enabled and records the kit name
in applied_kits. Clearing a kit removes its frame IDs from enabled and its
name from applied_kits. Kits are transparent: you can see exactly which
frames they contain.
Stdlib ships six starter kits:
| Kit | Use case | Frame count |
|---|---|---|
core |
Universal floor: every project’s baseline. Default on nimblegate setup and on gateway add. |
18 |
web-app |
Projects shipping HTML pages (any backend). Includes core. |
30 |
cf-pages-project |
SvelteKit / Astro / Next on CF Pages with D1. Includes web-app. |
32 |
cf-workers-project |
Cloudflare Workers + Tunnels + Access, no HTML. Includes core. |
23 |
security-strict |
Adds every security/* frame on top of whatever else is applied (includes Trojan Source / tag-char / zero-width / homoglyph attack-class, plus PII detection). Stackable with any other kit. |
14 |
encoding-strict |
Adds every encoding/* frame: BOM, smart quotes, YAML tabs, line endings, mixed indent, en-dash flags, non-printable controls, ZWSP in docs. Stackable with any other kit. |
8 |
agent-shortcuts |
The ways a build is made to look green without being fixed: skipped or focused tests, tests that cannot fail, production code that detects test runs, test commands that run nothing, CI that ignores failures, linters switched off wholesale. All WARN. Stackable with any other kit. | 6 |
Kit CLI:
nimblegate kits list
nimblegate kits apply web-app
nimblegate kits clear web-app
nimblegate setup uses core by default. Pass --kit <name> to apply a
different kit on setup, or --kit none to start with an empty enabled list.
An empty enabled list is not “no frames” - it means every stdlib frame runs.
The list is an allowlist, consulted only when it is non-empty, so writing entries
into it narrows what is checked rather than adding to a baseline. Applying
core to a repo with an empty list takes it from all 57 frames down to that
kit’s 18. nimblegate gateway add leaves the list empty unless you pass
--kit; the dashboard’s Add form applies core for you.
Custom user kits#
Your project can define its own named kits in [[ui.user_kits]] blocks:
[[ui.user_kits]]
name = "MVP launch gate"
frames = [
"security/no-hardcoded-credentials",
"git-safety/folder-branch-lock",
]
Custom kits appear in the dashboard browse tree alongside the canonical categories, and as chips in the applied row.
CLI:
nimblegate kits create "MVP launch gate" --frames security/no-hardcoded-credentials,git-safety/folder-branch-lock
nimblegate kits delete "MVP launch gate"
kits delete removes the kit entry from [[ui.user_kits]] but does not
untick the frames it contained. If you want the frames unticked too, use
kits clear before deleting, or tick them off individually in the dashboard.
Tier metadata (sidenote)#
Each frame has a tier: 1–6 field used only for the time-prevented hours
estimate on the /stats page:
| Tier | Modeled hours per finding |
|---|---|
| 1 | 4 h |
| 2 | 2 h |
| 3 | 0.5 h |
| 4 | 0.25 h |
| 5 | 0.1 h |
| 6 | 0.1 h |
Tier is severity metadata, not a selection group. It never appears as a category, kit name, or selection primitive in any operator-facing surface.
Per-tier hour values can be overridden on the Policy page → Time-prevented estimates if the stdlib defaults don’t fit your cost model.
Migration from @-prefixed group configs#
The @group-name config syntax was removed in v0.1.0. Any config with
@-prefixed entries or category wildcards in enabled will be rejected on
load with an error message listing the equivalent kit.
Migration table:
@tier-1 → nimblegate kits apply core
@tier-6 → tick Documentation category frames in dashboard
@web → nimblegate kits apply web-app
@cloudflare → nimblegate kits apply cf-workers-project
@cf-pages → nimblegate kits apply cf-pages-project
@migrations → tick Database > Migrations frames in dashboard
@security-strict → nimblegate kits apply security-strict
security/* → tick frames individually in dashboard
After updating your config, re-run to verify:
nimblegate kits list
nimblegate frames list
Severity: what happens at the gate#
Three severity values; one per finding. The operator can override a frame’s default severity per-repo from the Policy page.
| Severity | Gate behaviour |
|---|---|
BLOCK |
Push rejected synchronously; never reaches upstream. |
WARN |
Push allowed and relayed; finding recorded in audit log + dashboard feed. |
INFO |
Push allowed; finding recorded, shown less prominently. |
A useful rollout pattern: enable a new frame at WARN first, watch how often
it fires on real pushes for a week, then upgrade to BLOCK once you’ve
confirmed the false-positive rate is acceptable.
Triggers: where frames fire#
| Trigger | What runs it |
|---|---|
pre-commit |
Local nimblegate install on the dev box |
cli |
Explicit nimblegate check invocation |
pre-receive |
The gateway on push arrival |
post-receive |
The gateway after the gate accepts |
git-wrap |
The dev box’s git wrapper when commands run |
Most stdlib frames declare [pre-commit, cli]. The gateway runs them via the
pre-receive path regardless of the declared triggers.
Lifecycle#
| Value | Meaning |
|---|---|
active |
In the catalog; runs when enabled. |
proposed |
Drafted but not ready for general use. Not in any stdlib kit by default. |
deprecated |
Superseded; kept readable for the replacement chain. |
archived |
Premise no longer applies; removed from active use. |
The dashboard /frames page shows active only by default.
Patterns: the shared concept catalog#
Some failure shapes recur across multiple frames. The
internal/stdlib/frames/patterns/ directory documents these patterns:
abstract failure shapes that frames reference via the pattern: frontmatter
field. Examples:
quota-window-mismatch: API rejects silently when the request range exceeds an implicit per-dataset cap.secret-in-source: credential committed to a repo where it can be exfiltrated.ambiguous-config-value: config value that allows multiple valid interpretations.
Patterns aren’t checks themselves; they’re shared vocabulary the catalog uses to talk about failure shapes consistently. See pattern catalog for the full reference.
Authoring new frames#
Frame files live at internal/stdlib/frames/<dir>/<name>.md. The frontmatter
fields are documented in the anatomy section above. For a step-by-step guide
to writing and testing a new frame, see Writing your own frames.
Where to go next#
- Full per-frame list: Frame reference
- Write a new frame: Writing your own frames
- Look at a frame’s source:
internal/stdlib/frames/<dir>/<name>.md - Live catalog with tick state: dashboard
/framespage