Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Identify dangerous API footguns, surprising default behaviors, and sharp edges in codebases and dependencies. Adapted from Trail of Bits. Use during code review to catch APIs that are easy to misuse, configurations that surprise, and abstractions that leak.
.claude/skills/sharp-edges/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-21 | ✗→✓ | ▲ Improved | — | — |
| case-20 | ✗→✓ | ▲ Improved | — | — |
| case-15 | ✓→✓ | = Same ✓ | — | — |
| case-14 | ✗→✗ | = Same ✗ | — | — |
| case-22 | ✗→✗ | = Same ✗ | — | — |
Sharp edges are APIs, configurations, and patterns that are easy to use incorrectly. They work in the happy path but break in subtle, dangerous ways.
When evaluating sharp edges, consider three types of users:
APIs whose defaults do something unexpected:
typescript// SHARP: parseInt without radix parseInt("08") // 0 in old engines (octal), 8 in modern parseInt("08", 10) // Always 8 // SHARP: Array.sort() without comparator [10, 2, 1].sort() // [1, 10, 2] -- sorts as strings! [10, 2, 1].sort((a, b) => a - b) // [1, 2, 10] // SHARP: JSON.parse reviver runs bottom-up JSON.parse('{"a": {"b": 1}}', (key, val) => { // 'b' fires before 'a' -- counterintuitive }) // SHARP: fetch() doesn't reject on HTTP errors const res = await fetch('/api') // 404 doesn't throw! if (!res.ok) throw new Error(`HTTP ${res.status}`)
Operations that fail without telling you:
typescript// SHARP: Object.freeze is shallow const obj = Object.freeze({ nested: { value: 1 } }) obj.nested.value = 2 // Succeeds! Only top level is frozen // SHARP: Map vs Object key coercion const map = new Map() map.set(1, 'number') map.set('1', 'string') map.get(1) // 'number' -- Map preserves key types // But: const obj = {} obj[1] = 'number' obj['1'] = 'string' obj[1] // 'string' -- Object coerces keys to strings // SHARP: Promise.all fails fast Promise.all([p1, p2, p3]) // If p1 fails, p2/p3 results are lost Promise.allSettled([p1, p2, p3]) // Always returns all results
typescript// SHARP: == vs === null == undefined // true 0 == '' // true false == '0' // true // Always use === // SHARP: typeof null typeof null // 'object' -- historical bug, never fixed // SHARP: NaN NaN === NaN // false Number.isNaN(x) // Use this instead of x === NaN
typescript// SHARP: async forEach doesn't await [1, 2, 3].forEach(async (item) => { await processItem(item) // Fires all at once, doesn't wait }) // Use for...of instead for (const item of [1, 2, 3]) { await processItem(item) } // SHARP: Race condition in check-then-act const exists = await db.findOne({ email }) if (!exists) { await db.create({ email }) // Another request might create it between check and act } // Use upsert or unique constraint instead
typescript// SHARP: URL parsing inconsistencies new URL('http://evil.com\\@good.com') // Different browsers parse differently // SHARP: RegExp without anchors /admin/.test('not-admin-page') // true! No ^ or $ // SHARP: Timing attacks on string comparison if (userToken === storedToken) { } // Vulnerable to timing attack // Use crypto.timingSafeEqual instead // SHARP: Path traversal via join path.join('/uploads', userInput) // '../../../etc/passwd' works! path.resolve('/uploads', userInput) // Still dangerous // Validate that result starts with base directory
typescript// SHARP: MongoDB operator injection db.users.find({ username: req.body.username }) // If req.body.username = { "$ne": "" }, returns all users! // Sanitize: validate input is a string // SHARP: SQL LIKE injection db.query(`SELECT * FROM users WHERE name LIKE '%${input}%'`) // Input: "%" returns all, "_" matches any char // Use parameterized queries with ESCAPE clause // SHARP: ORM lazy loading in loops (N+1) const users = await User.findAll() for (const user of users) { const posts = await user.getPosts() // N+1 queries! } // Use eager loading: User.findAll({ include: Post })
typescript// SHARP: React useEffect cleanup race useEffect(() => { let cancelled = false fetchData().then(data => { if (!cancelled) setState(data) // Without this, stale updates }) return () => { cancelled = true } }, []) // SHARP: Express middleware order matters app.use(cors()) app.use(helmet()) app.use(authMiddleware) app.use(rateLimiter) // If rateLimiter is AFTER auth, unauthenticated requests aren't limited // SHARP: Next.js revalidate: 0 is NOT "no cache" // revalidate: 0 means "revalidate on every request" (still caches) // Use { cache: 'no-store' } for truly no cache
For each API/function/config in review:
[ ] What happens with empty/null/undefined input?
[ ] What happens with extremely large input?
[ ] What happens with concurrent access?
[ ] What happens when the network is slow/down?
[ ] What are the default values? Are they safe?
[ ] Does it fail silently or loudly?
[ ] Is the error message helpful or misleading?
[ ] Will a future developer understand the constraints?
[ ] Is there a safer alternative API?When you find a sharp edge, document it:
SHARP EDGE: [API/pattern name]
SURPRISE: [What happens that developers don't expect]
DANGER: [What can go wrong -- security, data loss, correctness]
FIX: [The safe alternative]
AFFECTED: [Which files/modules in this codebase use it]Inspired by Trail of Bits sharp-edges plugin.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-24 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
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. 24 cases were attempted. The headline lift of 0 percentage points is the difference between those two pass rates over the 24 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.