suede-code-review — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited suede-code-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.
Review code with full context: changed files, callers, contracts, deploy surface. Find real breakage. Rank by production impact. Every finding has a file, evidence, and a fix path. No findings without evidence. No volume without signal.
Default: Sonnet. Recommend Opus for auth, payments, and public API surface reviews.
switch into review mode and look for what your implementation would miss.
files.
or local conventions already handle them.
Before review, identify:
public copy, analytics, secrets, deployment, and docs.
Build a lightweight graph before judging the diff:
config/env dependencies touched by the change.
should move with the behavior.
dashboard, App Store metadata, or deployment target.
media, royalty routing, agent commerce, wallet/payment flows, and public claim truth.
Flag beyond-the-diff risks when related files, defaults, docs, env, or deploy requirements no longer agree.
Do not hand-review for what a tool already decides. Before manual analysis, run the gates the repo already ships and fold the output into findings. Detect what exists from package.json scripts, config files, and lockfiles — run only those. Never introduce a tool the repo does not use, and never fabricate a result you did not run.
typecheck script, or the type checker directly. An erroron a changed line is at least P2; on a changed critical path, P1.
change introduces; ignore pre-existing noise outside the diff.
behavior is P1; a test that silently stopped running is P2.
command, or a vulnerability scanner — when dependencies changed. A known-exploitable CVE reachable in a production path is P0/P1.
provides one — it catches keys the Commit Dirt Score patterns miss. A verified live secret is P0.
Cite the command, its exit status, and the file:line it implicates. If a gate cannot run (no script, missing deps, sandboxed), say so in Verification — never report a gate as passed that you did not execute.
A review that fights the house style produces noise, not signal. Honor what the repo already encodes.
CLAUDE.md, AGENTS.md,CONTRIBUTING.md, .editorconfig, formatter/linter config, and any review-config file at the repo root or in the changed directories. A documented convention is binding — do not flag what a rule permits; do flag what it forbids.
AGENTS.md overrides a repo-root rule for files under that path.
deliberate house pattern, capture it in one line — pattern, why it is allowed, path scope — and do not re-raise that pattern this review or in later ones. Repeat nitpicks erode trust faster than a missed P3.
personal preference. A style choice not governed by a rule, a formatter, or a real cost is not a finding.
release, or cross-repo risk.
acceptance criteria, test mapping, and missing decisions.
original finding is gone.
metadata, deployment, and live/API readback.
Add a --depth modifier to any review:
secrets, missing null checks, SQL/command injection patterns, broken error handling. No cross-file analysis. Use for PRs with narrow blast radius.
call chain tracing. Finds semantic bugs that only appear when you follow data across module boundaries. Use for auth changes, payment flows, data migrations, and public API changes.
State depth level at the top of every review output.
Check these on every TypeScript file in the diff:
any disables type checking for everything downstream. If the type is truly unknown at the call site, use unknown and narrow with a type guard. Flag every any annotation that isn't in a third-party type shim.foo!.bar is a runtime crash waiting for the condition that makes foo null. Flag unless the null case is provably eliminated by a guard two lines above.{ type: 'a', ... } | { type: 'b', ... }, the switch/if must be exhaustive. Missing default: assertNever(x) is a silent future bug.as that isn't a DOM cast (as HTMLInputElement) or a narrow type refinement proven by the preceding condition.Object.keys(obj).forEach(k => obj[k]) fails type checking silently at runtime when keys are typed. Require (Object.keys(obj) as Array<keyof typeof obj>) or a typed Object.entries().return someAsyncFn() inside an async that should return await someAsyncFn().Check these on every React component or hook in the diff:
useEffect(fn) (no array) runs on every render. useEffect(fn, []) runs once. useEffect(fn, [dep]) runs when dep changes. Flag any effect where the deps array is absent or obviously incomplete (function references, object literals, values used inside the effect but not listed).setInterval inside useEffect(fn, []) that reads a stateful value. The fix is either adding the dep or using a ref..map() returning JSX elements must have a stable, unique key. Using array index as key is a bug when the list can reorder or filter. Flag index-keyed lists where items have identity (id, slug, etc.).useMemo/useCallback when those objects are created inline in the parent render. Flag components that could be wrapped in React.memo but aren't, when rendered in tight loops or high-frequency update paths.arr.push(item) on state does not trigger a re-render. Flag any direct mutation of state variables.Check these on every React component or HTML-producing file in the diff:
<img> without alt, or alt="" on non-decorative images. Empty alt skips the element for screen readers; flag when the image conveys content.<button>, <a>, <input> with no visible text, no aria-label, and no aria-labelledby. An icon-only button with no label is invisible to assistive tech.<div onClick={...}> used as a button; <span> used as a link; heading levels skipped (h1 → h3) or used for visual styling instead of document hierarchy. Use the correct element or add role= with keyboard handlers.onClick but not onKeyDown (Enter, Space, arrow keys). Flag interactive elements that can't be reached or operated by keyboard alone.text-gray-400 on white, text-white on light-colored buttons, any low-contrast pairing below ~4.5:1 for body text or ~3:1 for large text.<input> or <select> without an associated <label> (via htmlFor/id) or aria-label. Placeholder text is not a label.aria-hidden="true" on interactive elements (traps keyboard users); role="presentation" on semantically meaningful elements; ARIA roles that contradict the native element's semantics.Check these on every Next.js file in the diff:
'use client' cannot import server-only modules (DB clients, fs, crypto, server-side env vars). Any file without 'use client' that uses useState, useEffect, browser globals (window, document), or event handlers is a runtime crash. Flag the mismatch, not just the symptom.<Suspense fallback={...}> wrapper. Missing boundaries cause the entire parent tree to suspend without a fallback.process.env.SECRET_KEY in a 'use client' file or in a prop passed from server to client component is exposed in the browser bundle. Only NEXT_PUBLIC_* vars are safe client-side. Flag any non-NEXT_PUBLIC_ env var referenced in client code.{ next: { revalidate: N } } or unstable_cache wrapping.next/navigation. Importing from next/router in an App Router project silently fails or returns stale data. Flag the wrong import.Check on any file that touches routes, layouts, metadata, or public-facing content:
generateMetadata, <Head>, <title>, description, og:title, og:description, og:image, twitter:card. Any removal or blanking of previously populated fields is a regression. Flag if generateMetadata now returns fewer keys than before the change.canonical URL changed, removed, or now pointing to a different domain. Flag any change to canonical logic — canonical changes can consolidate or fragment link equity unintentionally.noindex, nofollow, or X-Robots-Tag: noindex appearing on pages that were previously indexable. Flag any new robots metadata that restricts crawling on a route that wasn't restricted before.sitemap.ts / sitemap.xml not updated when routes change. Flag route additions or removals without a corresponding sitemap change.og:image URLs broken, pointing to localhost, missing dimension params, or removed from previously covered pages. Flag layout-level changes that remove OG image generation entirely.schema.org markup changed, fields removed, or @type changed. Flag removals of @type, name, url, or description from any existing structured data block.llms.txt or llms-full.txt, flag any changes that expand or restrict what AI crawlers are permitted to see.Check these on any file that touches DB queries:
WHERE id IN (...) or a join. Flag any .map() or for loop that calls db.query(), prisma.find*(), or db.select() inside the body.WHERE, ORDER BY, GROUP BY, or join condition on a column that isn't indexed is a full-table scan. Flag new query predicates on columns with no corresponding index in the migration/schema.INSERT/UPDATE/DELETE calls that must succeed or fail together. Flag any multi-table write that isn't wrapped in db.transaction() / prisma.$transaction().UNIQUE constraint are race-condition bugs at scale. Flag schema definitions where uniqueness is enforced in application code but not the DB.db.select().from(table) with no LIMIT on a user-facing route is a DoS vector and a cost spike. Flag selects with no limit when the table can grow.import of a package not already in the bundle. If the package's minzipped size is >20 KB, flag as P3 with the size estimate. Prefer tree-shaken imports (import { X } from 'pkg' not import pkg from 'pkg').<script src="..."> without async or defer in HTML head blocks page paint. Flag in any HTML template or _document.tsx.import at the top of _app.tsx or a layout file when it could be next/dynamic with ssr: false. Flag routes not in the critical path (settings pages, dashboards, modals).<img src="..."> bypasses Next.js image optimization. Flag raw <img> tags in Next.js files pointing to non-SVG assets.For --deep reviews on auth changes, payment flows, data migrations, or public API changes, run as separate lanes:
Collect consensus first. If high-severity concerns persist after a fix cycle, keep status at hold and name the smallest next check or patch.
The changed-file import graph is the floor. The bugs that ship hide outside the diff. On --deep, widen past the immediate callers:
git log -L or git blame the changed region. If this area was fixed before, a change that reintroduces the old shape is a regression — cite the prior commit. If it churns repeatedly, flag it as fragile.State what you traced. A whole-repo claim with no symbols named is not evidence.
Check on any new route handler, API endpoint, background job, cron, queue consumer, or service function:
try/catch blocks that catch without logging or Sentry capture. Flag catch (e) {} and catch (e) { console.error(e) } — a caught error that only console.errors is invisible in production. Require Sentry.captureException(e) or equivalent on unexpected errors.Run this automatically at the end of every review. No exceptions. It answers one question: is this safe to deploy right now?
Grade each dimension. Each is pass / conditional / block:
git revert fully undo this? Red flags: schema migrations, irreversible external API calls (email sent, payment charged, data permanently deleted), S3/storage mutations, message queue publishes. Block if rollback requires manual data repair.~0% (new feature, flagged), ~partial (specific flow), ~100% (shared middleware, auth, DB query in hot path). Higher blast radius requires more evidence before deploy..github/workflows/ (or equivalent CI) exist and cover the changed surface (build, types, tests)? Are required status checks enforced on main? Block if the changed surface has no automated gate and the repo is production-connected.Output block — required at the end of every review:
DEPLOY SAFETY
Breaking changes: pass | conditional | block — [evidence]
Rollback safety: pass | conditional | block — [evidence or red flag]
Blast radius: ~X% — [which path or user segment]
Environment readiness: pass | conditional | block — [missing vars if any]
Dependency changes: pass | conditional | block — [new deps and CVE status]
Data mutations: pass | conditional | block — [irreversible operations if any]
Security delta: improved | neutral | block — [surface changed]
Verdict: SAFE TO DEPLOY | DEPLOY WITH CONDITIONS | DO NOT DEPLOY
Conditions (if any):Also run the $suede-code-grader rubric for the A-F lane grades:
Code grade:
Correctness: A-F
Security and permissions: A-F
Data and state: A-F
Suede truth: A-F
UX and release behavior: A-F
Tests and verification: A-F
Deploy readiness: A-F
Overall: A-F
Why:
Required upgrades:Run automatically on every review. Scan the raw diff for content that should never reach git history. No exception for "it's just a branch" — dirty commits propagate.
Check every line added (`+`) in the diff for:
.env variable values hardcoded in source. Patterns: sk_, pk_, Bearer , -----BEGIN, password=, secret=, token=, AKIA, ghp_, xoxb-, long hex/base64 strings adjacent to key-like identifiers.console.log, console.error, debugger, print(, pprint(, binding.pry, byebug, dd(, dump(, var_dump(, hardcoded test user IDs, TODO: remove, FIXME: before merge, HACK:, commented-out blocks of real logic.<<<<<<<, =======, >>>>>>>, |||||||. A conflict marker in a + line means the file was not fully resolved.node_modules/, .next/, dist/, build/, *.pyc, __pycache__/, .DS_Store, *.log, *.sqlite, *.db, migration snapshot files that weren't meant to commit, generated lock-file noise from the wrong package manager.[WIP], DO NOT MERGE, TEMP:, SKIP CI in non-CI files, stash this, placeholder copy like lorem ipsum, foo, bar, asdf in production-facing strings.public/ or assets/ directory.Score:
COMMIT DIRT SCORE
Secrets / credentials: clean | suspicious | dirty — [pattern found or "none"]
Debug artifacts: clean | suspicious | dirty — [symbol or line or "none"]
Conflict markers: clean | dirty — ["none" or file:line]
Accidentally staged: clean | dirty — [path or "none"]
WIP breadcrumbs: clean | suspicious | dirty — [marker or "none"]
Oversized / binary: clean | dirty — [file and size or "none"]
Exposed internals: clean | suspicious | dirty — [pattern or "none"]
Overall dirt rating: CLEAN | SUSPICIOUS | DIRTYA DIRTY overall rating automatically sets the Ship Gate to hold.
Lead with findings, ordered by severity. Group repeated patterns once: "This pattern appears in 4 files: [list]. Fix described once below."
P0 / P1: use the full block:
[P0] path/to/file.ts:142
Issue: JWT secret falls back to empty string; any token is valid when SECRET is unset.
Fix: `process.env.JWT_SECRET ?? (() => { throw new Error('JWT_SECRET required') })()`
Verify: set JWT_SECRET="" and curl /api/me — expect 401, currently 200.
OWASP: A02 Cryptographic Failures
Confidence: highP2 / P3: one line each:
[P2] components/Feed.tsx:88 — missing key prop on .map() return; use item.id not index. TS will catch post-fix.
[P3] utils/format.ts:12 — magic number 86400; extract as SECONDS_PER_DAY.Severity:
If the issue cannot be tied to a file, route, command, state, or user-visible behavior, mark it as an open question instead.
When asked to fix, convert each accepted finding into an agent-ready brief:
Fix one risk cluster at a time. After fixing, rerun the relevant review mode and do not close the finding until evidence confirms it.
When the user asks to apply fixes (--fix flag or equivalent instruction):
auto-apply to production-critical, auth, payment, or data-migration code without explicit user confirmation.
files. Mark original findings as resolved or escalated.
attempts, escalate as a design issue requiring human decision.
OWASP check runs automatically on any file in: auth/, api/, middleware/, routes/, pages/api/, or any file importing a crypto, session, or payment module. Cite the OWASP category in the finding.
access? Are admin routes protected? Does the code prevent horizontal privilege escalation (user A accessing user B's data)?
transit? Are deprecated algorithms (MD5, SHA-1, DES) used? Are secrets in environment variables, not source?
commands, LDAP, XML, or template engines? Is output escaped before rendering in HTML?
unauthenticated? Are rate limits designed in, not bolted on?
disabled? Are error messages safe (no stack traces to users)? Is debug mode off in production?
present? Are unused packages removed?
sensitive actions? Are brute-force protections present? Are sessions invalidated on logout?
Are package checksums verified? Are deserialized inputs validated?
control failures, and input validation failures logged? Are logs protected from tampering?
tricked into fetching internal network resources?
Always check these when relevant:
claims match implemented behavior;
release promises;
claims;
the actual app;
are documented and deployable;
mobile, public sites, and docs.
Check on every Swift file in the diff. iOS ships on a release cycle with no hot-fix — a crash here is live for days.
try! on anything that throws at runtime (decoding, file I/O) is P1.self inside a stored property, Task, Combine sink, or callback leaks the owner. Require [weak self] (or [unowned self] only when lifetime is provably bound).@State, @Published, or UIKit views from a background context is undefined behavior. Flag observable or UI mutation inside Task.detached, a URLSession callback, or a background queue with no hop to @MainActor / DispatchQueue.main.nonisolated used to silence a warning rather than because access is truly isolation-free; @MainActor work awaited from a path that should already be on main.decode on fields the backend does not guarantee.ForEach over a non-stable id (index, or a value that changes) loses state and breaks animations; onAppear used for one-time work re-fires on re-insertion; @StateObject vs @ObservedObject confusion (owning vs observing) re-creates or prematurely releases models.Pair with iOS / Native Contract Drift below: crash risk here, contract break there.
Check on any API route, response shape, auth header, or shared type that iOS or other native consumers depend on:
Authorization, X-Session-Token, or similar headers are validated server-side. If the server changes the expected format, the iOS app gets 401s on every request.{ error: string } or { code: number }. Changing the error envelope silently breaks native error handling with no visible failure on the web surface.types/, shared/, or lib/ consumed by both web and a native build step. A field rename or removal here breaks both surfaces simultaneously.Blast radius note: identify which iOS version is currently in production. If the app hasn't shipped an update in >2 weeks, assume the old contract is live for the majority of users and treat breaking changes as P0.
Flag tech debt patterns as P3, group by file, don't block ship. Do not block a ship on P3 tech debt unless it directly obscures a P0/P1 bug. File as follow-up.
Code that works but does not do what it claimed is still a defect.
For a PR summary or any review spanning four or more files, lead with a walkthrough so the reader sees the shape before the findings:
sequenceDiagram (request → handler → service → data) or flowchart when the change moves data across three or more boundaries or alters an async, auth, or payment path. Skip it for a localized edit — a diagram of a one-file change is noise.The walkthrough orients; it never replaces findings, and stays above the Findings block.
False positives are why reviewers get muted. Self-check the draft findings before emitting:
For a review:
Findings
Code Grade
Open Questions
Verification Checked
Ship GateFor no findings, say that clearly and name any residual risk or unrun checks.
For a PR summary:
Summary
Change Map
Risk Map
Verification
Ship GateThe ship gate is always the last line of a review output:
SHIP GATE: hold | ship-with-caveats | ship
Reason: [one sentence, naming the blocking finding ID or named caveat]hold: a blocker or high-risk unknown remains.ship-with-caveats: no blocker remains, but named non-critical caveats exist.ship: no known blocker remains and required verification passed.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.