avoid-feature-creep
Keeps a change scoped to what was asked. Flags extra features, refactors, and 'while we are here' edits, and moves them to a…
Ten minute security pass over a Convex backend: public functions that should be internal, missing auth checks, unvalidated args, IDs from the client trusted without ownership checks, secrets in code. Use before merging a pull request, after adding new public functions, or when
$ npx -y skills add waynesutton/builder-skills --skill convex-security-check --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/convex-security-checkContext preview
The summary Claude sees to decide when to auto-load this skill.
Ten minute security pass over a Convex backend: public functions that should be internal, missing auth checks, unvalidated args, IDs from the client trusted without ownership checks, secrets in code. Use before merging a pull request, after adding new public functions, or when
name: convex-security-check description: Ten minute security pass over a Convex backend: public functions that should be internal, missing auth checks, unvalidated args, IDs from the client trusted without ownership checks, secrets in code. Use before merging a pull request, after adding new public functions, or when the user says 'quick security check'.
A ten minute pass over `convex/` that catches the mistakes that ship most often. The one rule: every exported `query`, `mutation`, and `action` is a public endpoint anyone can call with any arguments, so each one must check identity and ownership or be intentionally anonymous.
Run each grep, read what it surfaces, fix or file. This skill finds problems; it does not map the whole system. For that, hand off to convex-security-audit.
Run the commands from the project root. Every grep excludes `_generated`.
Anything only called by other Convex functions, the scheduler, crons, or webhooks should be `internalQuery`, `internalMutation`, or `internalAction`.
rg -n "export const \w+ = (query|mutation|action)\(" convex --glob '!**/_generated/**'
rg -n "\bapi\.\w+\.\w+" convex --glob '!**/_generated/**'The second grep finds `api.*` used inside the backend. Server code scheduling or running a public function is almost always a sign that function should be internal.
rg -l "= (query|mutation|action)\(" convex --glob '!**/_generated/**' \
| xargs rg -L "getUserIdentity|getCurrentUser|authedQuery|authedMutation"Files listed contain public functions and no auth call at all. Open each one. A public post list is fine anonymous. Anything that returns or writes user data is not.
rg -nU "(query|mutation|action)\(\{\s*handler" convex --glob '!**/_generated/**'
rg -n "v\.any\(\)" convex --glob '!**/_generated/**'The first grep finds functions whose definition starts with `handler`, meaning no `args`. The second finds `v.any()`, which turns off validation for that field.
`v.id("tasks")` proves the string is a valid ID for that table. It does not prove the caller owns the document.
rg -n "ctx\.db\.(get|patch|delete|replace)\(args\." convex --glob '!**/_generated/**'
rg -n "userId: v\.(id|string)\(" convex --glob '!**/_generated/**'For each hit in the first grep, find the ownership comparison between the read and the write. For the second, a public function that accepts the caller's own `userId` as an argument is trusting the client to say who it is.
rg -n -i "(sk_live|sk_test|whsec_|AKIA[0-9A-Z]{16}|-----BEGIN|api[_-]?key\s*[:=]\s*['\"][A-Za-z0-9])" convex src --glob '!**/_generated/**'
rg -n "process\.env\." srcAnything in `src/` that reads `process.env` or `import.meta.env` ships to the browser. Only deployment URLs and public client IDs belong there.
rg -n "http\.route|httpAction\(" convex/http.ts
rg -n "storage\.(generateUploadUrl|getUrl)" convex --glob '!**/_generated/**'The most common finding: auth is checked, ownership is not.
Before:
export const updateTask = mutation({
args: { taskId: v.id("tasks"), title: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new ConvexError("Sign in required");
// any signed in user can rename any task
await ctx.db.patch(args.taskId, { title: args.title });
return null;
},
});After:
import { mutation } from "./_generated/server";
import { v, ConvexError } from "convex/values";
import { getCurrentUser } from "./lib/auth";
export const updateTask = mutation({
args: { taskId: v.id("tasks"), title: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const user = await getCurrentUser(ctx); // throws when signed out
const task = await ctx.db.get(args.taskId);
// same error for missing and not owned, so IDs cannot be probed
if (!task || task.userId !== user._id) {
thAgent skills for builders shipping Convex apps. By Wayne Sutton. Seventeen skills.
Keeps a change scoped to what was asked. Flags extra features, refactors, and 'while we are here' edits, and moves them to a…
Builds AI agents on the Convex agent component: threads, messages, tools that call queries and mutations, streaming, RAG with…
Production patterns for Convex apps and the rules the @convex-dev/eslint-plugin enforces: validators, indexes, idempotent…
Creates reusable Convex components with defineComponent, a clean client wrapper, their own schema, and an npm publish setup. Use…
Schedules work in Convex: cron jobs in convex/crons.ts, one off scheduled functions with runAfter and runAt, batching large jobs,…
Handles files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action, metadata from the…