Claude Code skills are Markdown files that teach Claude a repeatable procedure. You drop one in .claude/skills/<name>/SKILL.md and call it with /<name>, or Claude picks it up when your request matches its description.
Here is the code review skill I use every day, complete and free to copy, plus an unedited run of it against a deliberately broken auth refactor.
The skill
Save this as .claude/skills/review/SKILL.md in your repo (or in ~/.claude/skills/review/SKILL.md to have it everywhere):
---
name: review
description: "Reviews the current diff (or a branch) for bugs, security issues and debt, and returns prioritized findings. Use when the user says \"review\", \"code review\" or \"what do you think of these changes\"."
---
# Code review
1. Get the diff: `git diff` + `git diff --staged`, or `git diff <base>...HEAD` if the user names a branch.
2. Read the context you need from every touched file (not just the diff lines).
3. Look for, in this order:
1. **Correctness**: edge cases, null/undefined, async without `await`, race conditions, off-by-one, error handling that swallows exceptions.
2. **Security**: injection (SQL, shell, XSS), secrets in code, input validation, missing authorization in resolvers/endpoints, sensitive data in logs.
3. **Data and API**: irreversible migrations, breaking contract changes (REST/GraphQL), N+1 in resolvers.
4. **Performance**: unnecessary React renders, effects with wrong dependencies, queries in loops.
5. **Maintainability**: duplication, confusing names, dead code, missing tests for new logic.
4. Return a table ordered by severity:
| Sev. | File:line | Problem | Suggestion |
|------|-----------|---------|------------|
| ๐ด high / ๐ก medium / โช minor | ... | ... | ... |
5. Only report what you can justify with a concrete scenario. If you find nothing serious, say so. Do not apply changes unless the user asks.
Three details make it work better than "review my code":
- It reads the whole touched file, not just the diff. Most real bugs live in how the changed lines interact with the unchanged ones.
- A fixed checklist order. Correctness and security come before style, so the important findings don't get buried under naming nits.
- "Only report what you can justify with a concrete scenario." This one line removes most of the vague, generic advice LLM reviews tend to produce.
A real run
I made a test repo with an auth refactor that introduces three bugs on purpose: an auth bypass, a secret written to the logs, and a SQL injection. Then I staged it and typed /review. This is the output, unedited (Claude Code 2.1.293):
| Sev. | File:line | Problem | Suggestion |
|---|---|---|---|
| ๐ด high | src/middleware/auth.ts:8-9 |
Auth bypass. verifyToken now returns null for an invalid or expired token. The middleware still sets req.user = null and calls next(). A request with Authorization: Bearer garbage therefore passes requireAuth. Any handler that only checks the middleware, or reads req.user?.sub loosely, is exposed. |
After verifying, add if (!payload) return res.status(401).json({ error: "invalid token" }); before next(). |
| ๐ด high | src/auth/service.ts:21 |
SQL injection. findUserByEmail interpolates email straight into the query. An input like ' OR '1'='1 returns arbitrary users, and '; DROP TABLE users;-- can run further statements, depending on the driver. |
Use a parameterized query, e.g. db.query("SELECT * FROM users WHERE email = $1", [email]), with the placeholder syntax your driver uses. Replace db: any with a real type, and select only the columns you need instead of *. |
| ๐ด high | src/auth/service.ts:4 |
Secret in logs. console.log("auth init", config.JWT_SECRET) writes the JWT signing secret to stdout on every start. Anyone with log access can forge valid tokens. |
Delete the line. If you need a startup message, log without the secret. |
| ๐ก medium | src/auth/service.ts:13-17 |
catch {} swallows every error and returns null. A bad token, an expired token and a misconfigured secret all look the same, which makes failures hard to diagnose. jwt.verify can also return a string payload, so callers can't rely on .sub. |
Return a typed result, such as `{ sub: string } \ |
| ๐ก medium | {% raw %}src/middleware/auth.ts:6
|
replace("Bearer ", "") doesn't check the scheme. A header of Basic xyz becomes Basic xyz and is passed to verify. That is harmless once the bypass is fixed, but sloppy. |
Match with /^Bearer (.+)$/ and return 401 if it doesn't match. |
| โช minor | src/middleware/auth.ts:4 |
Dropping async is correct now that verifyToken is synchronous. |
None. This is fine as is. |
| โช minor | src/auth/service.ts |
There are no tests for verifyToken or findUserByEmail. A test for "invalid token returns 401" would have caught the bypass. |
Add tests for valid, expired, malformed and missing tokens. |
I recommend not merging until the three ๐ด items are fixed. I haven't changed anything. Tell me if you want me to apply the fixes.
All three planted bugs were found, ranked first, and each came with a concrete exploit scenario and fix. It also didn't touch any code, because the skill says not to unless asked.
Make it yours
- Add your stack's usual suspects to step 3 (e.g. "missing
keyin React lists", "unbounded GraphQL queries"). - If you want it to trigger without typing
/review, put the phrases you naturally use in thedescription("check my changes", "anything wrong here?"). - Keep the severity table: it makes the result scannable and easy to paste into a PR.
If you want more like this, the Starter Kit for Claude Code (US$9, unofficial) has 5 more skills in the same style (/commit, /pr, /tests, /plan, /debug), safety hooks that block rm -rf, force-push and leaked secrets (with 27 tests), CLAUDE.md templates for React, Node and GraphQL, and permission presets: https://luijhy.gumroad.com/l/starter-kit-for-claude-code
The safety hooks alone are free here: https://github.com/kailucho/claude-code-hooks-seguridad
Top comments (0)