remix-v2-forms-review — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited remix-v2-forms-review (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
See remix-v2-forms for canonical patterns. This skill flags violations; the sibling skill teaches the patterns.
| Issue Type | Reference |
|---|---|
Manual fetch(), native <form>, wrong <Form> vs useFetcher choice | references/form-vs-fetcher.md |
useState loading flags, useNavigation for per-row, missing pending state | references/pending-state.md |
Unbounded memory uploads, missing encType, unvalidated FormData, mirrored optimistic state | references/uploads-validation.md |
| Route sprawl instead of intent pattern, PUT/DELETE without PE fallback | references/multi-action-routes.md |
<Form> or <fetcher.Form>, never fetch() / axios<Form> is imported from @remix-run/react (not native <form>) for POST mutationsuseFetcher used when URL should NOT change (row toggle, inline edit)<Form> + redirect(...) used when URL SHOULD change (create, delete-then-list)useNavigation() or fetcher.state, never useStatefetcher.state (not page-global useNavigation)useNavigation() calls check navigation.formAction to scope to expected pathfetcher.formData / navigation.formData directly (not mirrored)redirect(...), returning json() only for errors / same-page<Form encType="multipart/form-data"> on every file-upload formunstable_createMemoryUploadHandler always has maxPartSize; large files use disk/stream handlerFormData values are validated/coerced before reaching the DB (no form.get(x) as string)method="put|patch|delete" is documented as JS-only, or rewritten as POST + intentnav.formMethod / fetcher.formMethod compared against UPPERCASE strings ("POST", "GET"); v2's v2_normalizeFormMethod default returns UPPERCASE — === "post" silently never matches.These are correct Remix v2 usage and should not be reported:
action is optional.redirect, encType) apply only to POST forms.onChange triggers.<fetcher.Form> without holding a fetcher ref.Only flag these when the listed condition holds:
| Issue | Flag ONLY IF |
|---|---|
Native <form> instead of <Form> | Method is POST and the route has an action — GET forms and external-URL forms are fine |
| Missing pending state | The form is POST and there is no useNavigation() / fetcher.state read anywhere in the component |
Action returns json({ ok: true }) after a create | The route is a "/new" or creation surface — same-page edit forms legitimately return JSON |
method="put" / "patch" / "delete" | Progressive enhancement is in scope for the surface (public app) — admin/JS-only tools may opt out if documented |
Unbounded unstable_createMemoryUploadHandler | The upload accepts user-controlled files (not a fixed-size internal artifact) |
| Separate routes per mutation | The mutations operate on the same resource with compatible auth — sibling resources with different rules are fine |
useNavigation() without formAction filter | The component contains other navigation surfaces (sidebar <Link>, sibling forms) that would trigger false positives |
Mirroring fetcher.formData into state | The shadowed value drives a user-visible element (button label, count, toggle) — a local "is-editing" flag is unrelated |
Run these in order. Do not draft user-facing findings until every gate passes for the batch you are about to report.
action if one exists.encType, missing redirect, or missing pending state, you have confirmed the form is method="post" (or put|patch|delete). GET forms are legitimate for search/filter and trigger loaders, not actions — applying POST-form rules to them is a false positive.fetch(), native <form>, wrong primitive: references/form-vs-fetcher.mduseState flags, page-global vs per-row, missing entirely: references/pending-state.mdencType, unvalidated keys, mirrored optimistic state: references/uploads-validation.mdfetch() / axios / native <form>, or choose between <Form> and useFetcher → form-vs-fetcher.mdunstable_* handlers, FormData parsing, optimistic UI → uploads-validation.mdaction (no manual fetch())?<Form> vs useFetcher choice driven by whether the URL should change?useNavigation() / fetcher.state (never useState)?fetcher.state (not page-global useNavigation)?encType="multipart/form-data" and use bounded handlers?<Form> rendering inside a non-route component is still tied to thenearest route's action — read the route file before flagging "missing action".
useActionData() returning data after a successful submission isexpected behavior; the data persists until the next navigation. Only flag if a success banner is rendered unconditionally without a dismiss path.
Form aliased (e.g. import { Form as RemixForm })is still the Remix component — match on import source, not local name.
Complete Hard gates (especially gate 4), then report only issues that still pass the review-verification-protocol pre-report checks.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.