Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Audit and harden Convex authorization: identity-from-arg impersonation, missing per-document ownership checks, PII-leaking public queries, and writes into containers the caller doesn't own. Deterministic scan + canonical requireIdentity/requireOwner fix + tsc verify. Use for 'secure my app' / 'audit auth' / 'who can access this data', not generic code review.
.claude/skills/get-convex-convex-authz/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 2436% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 132% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 129% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 37% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 143% | 0% |
<!-- GENERATED from convex-agents content/capabilities/convex-authz.json — do not edit by hand. -->
A focused authz specialist, not a general reviewer: it finds and fixes the four shapes that account for the largest real-defect cluster measured against generated Convex backends (25 identity-from-arg + 13 missing-ownership-check + 6 PII-leak-by-argument = 44 of 214 confirmed defects, plus the parent-reference-on-write variant of the ownership shape that fixture measurement showed the 3-shape scan misses). It runs a deterministic scan first (objective, regex-based), then applies the canonical requireIdentity/requireOwner hardening pattern from convex-expert.md to every hit, then verifies with tsc. It does not re-derive the pattern — it applies the one already documented as the platform's canonical fix.
/add auth or the auth setup first, then re-run convex-authz to add per-user ownership checks.' Do not run steps 1-3 below against public functions on a foundationless app beyond this internalize-and-defer move. Only when the foundation exists (both auth.config.ts and a subject-keyed users table are present) do you proceed to inject requireIdentity/requireOwner in steps 1-3.(a) identity-from-arg: a public query(/mutation( object whose args block declares userId/actorId/ownerId/authorId/accountId typed v.id(...), where the function's whole block (args + handler) has zero ctx.auth reference. Regex: /\b(userId|actorId|ownerId|authorId|accountId)\s*:\s*v\.id\(/ inside an args: { ... } block paired with an absent /\bctx\.auth\b/ anywhere in the enclosing (query|mutation)\(\s*\{ ... } block (word-boundary excludes internalQuery/internalMutation by construction). (b) missing-ownership-check: a public query(/mutation( whose handler loads a document via ctx.db.get(args.<xId>) (an _id-typed arg) and then calls ctx.db.patch/ctx.db.delete/ctx.db.replace on that same id, or returns the doc's fields directly, with no comparison of any <doc>.<ownerField> against an identity value anywhere in the block (no ===/!== involving identity.subject or a ctx.auth derived value). (c) PII-leaking public query: a public query( whose returns (or the raw doc it returns) includes a sensitive-looking field (email, revenue, ssn, password, token, auditLog, dashboard-shaped aggregate) and the query is parameterized by a client-supplied id with no ctx.auth check gating access to that id's own scope. (d) parent-reference ownership on write: a public mutation( whose args include a v.id(...) of a parent/container table (projectId, boardId, teamId, orgId, listId, folderId, conversationId, accountId, ...) that the handler uses as a foreign key in a ctx.db.insert/ctx.db.patch — attaching or moving a child row into that container — without verifying the caller owns (or is a member of) the referenced parent doc. Creating a row inside someone else's container is the same defect as mutating their row: fixing WHO the caller is (shape a) does not fix WHERE they may write. After handling shapes a-c, re-audit every REMAINING v.id(...) arg in every public mutation for this shape — shape-a fixes routinely leave the parent id arg behind, still unchecked. Report every hit with file, line, and which of the 4 shapes matched — this is the objective, model-independent baseline; do not skip it in favor of jumping straight to judgment.
convex/model/auth.ts exporting requireIdentity(ctx) (throws 401 if ctx.auth.getUserIdentity() is null; returns the identity) and requireOwner(ctx, doc) (throws 404 if doc is null, throws 403 if doc.ownerId !== identity.subject, else returns doc). Rewrite each flagged function: replace the client-supplied identity arg with requireIdentity(ctx); wrap each _id-keyed read/mutate with requireOwner(ctx, await ctx.db.get(args.xId)) before touching the row; scope each PII-returning query through requireIdentity/requireOwner (or an explicit staff/role check) before it reads outside the caller's own scope; for each shape-(d) hit, load the referenced parent doc and apply requireOwner(ctx, parent) (or the schema's membership check — e.g. participantIds.includes(user._id) — when the container models members as an array) BEFORE inserting/patching the child row. When the schema keys ownership by a users row id rather than the raw subject, resolve the caller's users row first (via the subject-keyed index) and compare against user._id — comparing an Id<"users"> field to identity.subject never matches and silently breaks enforcement. Never widen scope — an internal/admin function that legitimately operates on an arbitrary user stays internalQuery/internalMutation, never public; leave it unflagged and unchanged.npx tsc --noEmit (or the project's typecheck script) after edits; a hardening pass that doesn't typecheck is not done. Then re-run the step-1 scan to confirm 0 remaining hits (the fixed shapes no longer match the regexes because ctx.auth now appears in-block and ownership comparisons now exist).| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 6,062 | 41,619 | +587% | 1 | 1 | 0% | 402 | 6,441 | +1502% | 0 | 0 | — |
case-02 | fail→pass | 4,069 | 41,545 | +921% | 1 | 1 | 0% | 235 | 5,960 | +2436% | 0 | 0 | — |
case-03 | fail→pass | 21,882 | 40,405 | +85% | 1 | 1 | 0% | 3,011 | 6,995 | +132% | 0 | 0 | — |
case-04 | pass→pass | 17,133 | 8,791 | -49% | 1 | 1 | 0% | 2,709 | 3,801 | +40% | 0 | 0 | — |
case-05 | pass→pass | 13,645 | 7,736 | -43% | 1 | 1 | 0% | 2,111 | 3,470 | +64% | 0 | 0 | — |
case-06 | pass→pass | 9,315 | 17,203 | +85% | 1 | 1 | 0% | 1,732 | 4,718 | +172% | 0 | 0 | — |
case-07 | fail→fail | 10,547 | 15,653 | +48% | 1 | 1 | 0% | 1,991 | 4,471 | +125% | 0 | 0 | — |
case-08 | fail→fail | 14,037 | 10,130 | -28% | 1 | 1 | 0% | 1,979 | 4,060 | +105% | 0 | 0 | — |
case-09 | fail→pass | 9,234 | 5,028 | -46% | 1 | 1 | 0% | 1,242 | 2,844 | +129% | 0 | 0 | — |
case-10 | pass→pass | 9,553 | 2,105 | -78% | 1 | 1 | 0% | 1,543 | 2,335 | +51% | 0 | 0 | — |
case-11 | fail→fail | 14,011 | 5,059 | -64% | 1 | 1 | 0% | 2,066 | 2,917 | +41% | 0 | 0 | — |
case-12 | pass→pass | 11,207 | 6,687 | -40% | 1 | 1 | 0% | 1,511 | 3,091 | +105% | 0 | 0 | — |
case-13 | fail→pass | 15,373 | 7,775 | -49% | 1 | 1 | 0% | 2,319 | 3,170 | +37% | 0 | 0 | — |
case-14 | pass→pass | 18,876 | 13,173 | -30% | 1 | 1 | 0% | 2,691 | 4,617 | +72% | 0 | 0 | — |
case-15 | pass→pass | 15,454 | 18,723 | +21% | 1 | 1 | 0% | 2,172 | 5,022 | +131% | 0 | 0 | — |
case-16 | pass→pass | 16,106 | 2,700 | -83% | 1 | 1 | 0% | 1,882 | 2,569 | +37% | 0 | 0 | — |
case-17 | fail→fail | 21,447 | 8,573 | -60% | 1 | 1 | 0% | 2,695 | 3,398 | +26% | 0 | 0 | — |
case-18 | fail→pass | 13,507 | 14,844 | +10% | 1 | 1 | 0% | 1,733 | 4,211 | +143% | 0 | 0 | — |
case-19 | pass→pass | 4,902 | 8,005 | +63% | 1 | 1 | 0% | 865 | 3,279 | +279% | 0 | 0 | — |
case-20 | pass→pass | 10,282 | 7,317 | -29% | 1 | 1 | 0% | 1,359 | 3,471 | +155% | 0 | 0 | — |
case-21 | fail→fail | 15,423 | 30,843 | +100% | 1 | 1 | 0% | 3,090 | 6,700 | +117% | 0 | 0 | — |
case-22 | fail→pass | 12,116 | 7,649 | -37% | 1 | 1 | 0% | 1,897 | 3,140 | +66% | 0 | 0 | — |
case-23 | fail→pass | 27,299 | 8,439 | -69% | 1 | 1 | 0% | 2,428 | 3,277 | +35% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted, and 21 counted toward the lift figure. The other 2 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +30 percentage points is the difference between those two pass rates over the 21 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.