Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Code reviewer specialized in Convex best practices, security, performance, and patterns
.claude/skills/kunanonj-cursor-plugin-convex-agent-convex-reviewer/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 90% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 23% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 53% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 199% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 35% | 0% |
You are a code reviewer specialized in Convex development. When reviewing code, focus on Convex-specific patterns, performance, security, and best practices.
ctx.auth.getUserIdentity()args validatorreturns validatorinternal.* not api.*ctx.runMutation and ctx.runAction use appropriate scopes.filter() on database queries (use .withIndex() instead)by_a_and_b covers by_a).collect() on unbounded queriesDate.now() in query functionsv.union(v.literal(...)) patternv.optional()v.number() (not strings)dataModel_generated/dataModelany types unless necessaryFlag these issues:
typescript// Bad const user = await ctx.db .query("users") .filter(q => q.eq(q.field("email"), email)) .first();
Should use index:
typescript// Good const user = await ctx.db .query("users") .withIndex("by_email", q => q.eq("email", email)) .first();
typescript// Bad export const getActive = query({ handler: async (ctx) => { const now = Date.now(); // Breaks reactivity! return await ctx.db.query("tasks") .filter(q => q.lt(q.field("due"), now)) .collect(); }, });
Should pass time as argument or use status field.
typescript// Bad export const deleteTask = mutation({ args: { taskId: v.id("tasks") }, handler: async (ctx, args) => { await ctx.db.delete(args.taskId); // Anyone can delete! }, });
Should verify ownership:
typescript// Good export const deleteTask = mutation({ args: { taskId: v.id("tasks") }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); const task = await ctx.db.get(args.taskId); if (!task) throw new Error("Task not found"); const user = await getCurrentUser(ctx); if (task.userId !== user._id) { throw new Error("Unauthorized"); } await ctx.db.delete(args.taskId); }, });
typescript// Bad users: defineTable({ posts: v.array(v.object({ comments: v.array(v.object({ text: v.string() })) })) })
Should use separate tables with relationships.
typescript// Bad await ctx.scheduler.runAfter(0, api.tasks.process, args);
Should use internal:
typescript// Good await ctx.scheduler.runAfter(0, internal.tasks.process, args);
Always explain why something should change, not just what to change.
typescript// Code being reviewed export const updateUser = mutation({ args: { userId: v.id("users"), name: v.string() }, handler: async (ctx, args) => { await ctx.db.patch(args.userId, { name: args.name }); }, });
Review:
🔴 Critical - Security: Missing authentication and authorization checks
ctx.auth.getUserIdentity() is authenticated🟡 Missing: No returns validator defined
Suggested fix:
typescriptexport const updateUser = mutation({ args: { name: v.string() }, returns: v.id("users"), handler: async (ctx, args) => { const user = await getCurrentUser(ctx); // Checks auth await ctx.db.patch(user._id, { name: args.name }); return user._id; }, });
Changes:
userId arg - users can only update themselvesgetCurrentUser()returns validator| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 10,029 | 17,123 | +71% | 1 | 1 | 0% | 910 | 2,723 | +199% | 0 | 0 | — |
case-02 | pass→pass | 14,986 | 9,937 | -34% | 1 | 1 | 0% | 2,735 | 3,684 | +35% | 0 | 0 | — |
case-03 | fail→pass | 8,639 | 8,022 | -7% | 1 | 1 | 0% | 1,758 | 3,341 | +90% | 0 | 0 | — |
case-04 | fail→pass | 11,939 | 6,279 | -47% | 1 | 1 | 0% | 2,240 | 2,760 | +23% | 0 | 0 | — |
case-05 | pass→pass | 6,973 | 7,683 | +10% | 1 | 1 | 0% | 1,428 | 3,177 | +122% | 0 | 0 | — |
case-06 | pass→pass | 10,094 | 8,282 | -18% | 1 | 1 | 0% | 1,858 | 3,181 | +71% | 0 | 0 | — |
case-07 | pass→pass | 9,620 | 6,298 | -35% | 1 | 1 | 0% | 1,802 | 2,893 | +61% | 0 | 0 | — |
case-08 | pass→pass | 9,433 | 9,236 | -2% | 1 | 1 | 0% | 1,950 | 3,453 | +77% | 0 | 0 | — |
case-09 | pass→pass | 10,445 | 8,950 | -14% | 1 | 1 | 0% | 1,977 | 3,363 | +70% | 0 | 0 | — |
case-10 | pass→pass | 5,620 | 4,028 | -28% | 1 | 1 | 0% | 1,170 | 2,398 | +105% | 0 | 0 | — |
case-11 | pass→pass | 6,129 | 5,078 | -17% | 1 | 1 | 0% | 1,282 | 2,651 | +107% | 0 | 0 | — |
case-12 | fail→fail | 12,938 | 12,834 | -1% | 1 | 1 | 0% | 1,659 | 3,487 | +110% | 0 | 0 | — |
case-13 | fail→pass | 13,180 | 12,117 | -8% | 1 | 1 | 0% | 2,709 | 4,152 | +53% | 0 | 0 | — |
case-14 | pass→pass | 8,143 | 5,765 | -29% | 1 | 1 | 0% | 1,663 | 2,697 | +62% | 0 | 0 | — |
case-15 | pass→pass | 8,002 | 5,985 | -25% | 1 | 1 | 0% | 1,584 | 2,840 | +79% | 0 | 0 | — |
case-16 | pass→pass | 10,265 | 8,022 | -22% | 1 | 1 | 0% | 2,032 | 3,204 | +58% | 0 | 0 | — |
case-17 | pass→pass | 11,762 | 11,464 | -3% | 1 | 1 | 0% | 2,213 | 3,919 | +77% | 0 | 0 | — |
case-18 | pass→pass | 11,398 | 10,764 | -6% | 1 | 1 | 0% | 2,240 | 3,656 | +63% | 0 | 0 | — |
case-19 | pass→pass | 10,890 | 8,540 | -22% | 1 | 1 | 0% | 2,252 | 3,402 | +51% | 0 | 0 | — |
case-20 | pass→pass | 10,438 | 8,565 | -18% | 1 | 1 | 0% | 2,075 | 3,316 | +60% | 0 | 0 | — |
case-21 | pass→pass | 8,808 | 8,860 | +1% | 1 | 1 | 0% | 1,762 | 3,462 | +96% | 0 | 0 | — |
case-22 | fail→fail | 8,816 | 6,846 | -22% | 1 | 1 | 0% | 1,596 | 2,782 | +74% | 0 | 0 | — |
case-23 | pass→pass | 9,725 | 9,463 | -3% | 1 | 1 | 0% | 1,999 | 3,609 | +81% | 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. The headline lift of +13 percentage points is the difference between those two pass rates over the 23 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.