Sharp Edges
Living catalogue of confirmed hazard patterns in agent-studio. Each entry documents a real bug we've shipped. Invoke this skill during debugging and code review to check against known failure modes.
SE-01: Windows Backslash Paths
Symptom: Glob patterns match correctly in CI (Linux) but silently fail on developer machines (Windows).
Root cause: path.relative()
returns backslash-separated paths on Windows (node_modules\foo
), but glob patterns use forward slashes. [^/]*
in regex won't block
.
Fix:
// ALWAYS normalize before regex matching const rel = path.relative(root, filePath).replace(/\/g, '/');
Test assertion:
assert(normalizePath('a\b\c') === 'a/b/c'); assert(!normalizePath('a\b').includes('\'));
Files affected: .claude/lib/utils/path-constants.cjs , any glob-based exclusion logic.
SE-02: Prototype Pollution via JSON.parse
Symptom: Object.prototype gains unexpected properties after parsing malformed JSON from hook input or agent memory.
Root cause: JSON.parse('{"proto":{"isAdmin":true}}') silently mutates Object.prototype in older Node.js versions or when objects are created via Object.create(null) checks are missed.
Fix:
// Use safeParseJSON — strips proto, constructor, prototype keys const { safeParseJSON } = require('.claude/lib/utils/safe-json.cjs'); const { success, data } = safeParseJSON(rawInput, {});
Test assertion:
const before = Object.getPrototypeOf({}); safeParseJSON('{"proto":{"evil":true}}', {}); assert(!{}.evil);
Files affected: Any hook that calls JSON.parse() on stdin input.
SE-03: Hook Exit Code Protocol
Symptom: Hook appears to "block" when it should allow, or silently "allows" when it should block.
Root cause: Hooks use exit codes 0 (allow) and 2 (block). Using process.exit(1) or process.exit(true) does not block — it produces unexpected behavior.
Fix:
// CORRECT if (shouldBlock) process.exit(2); // Block tool execution process.exit(0); // Allow tool execution
// WRONG if (shouldBlock) process.exit(1); // Not a valid block code process.exit(false); // Not valid
Test assertion:
// Mock hook execution and verify exit code const result = execHook(input); assert(result.exitCode === 0 || result.exitCode === 2);
Files affected: All hooks in .claude/hooks/ .
SE-04: Async Exception Swallowing in Promise.all
Symptom: One failing async operation silently causes all results to be lost; agent believes all tasks succeeded.
Root cause: Promise.all([...]) rejects on the first failure and discards all other results. When wrapped in try/catch that returns [] , the caller sees empty results with no error.
Fix:
// Use Promise.allSettled to get partial results const results = await Promise.allSettled(tasks); const succeeded = results.filter(r => r.status === 'fulfilled').map(r => r.value); const failed = results.filter(r => r.status === 'rejected').map(r => r.reason); return { succeeded, failed, partial: failed.length > 0 };
Test assertion:
const tasks = [resolvesWith('ok'), rejectsWith('error'), resolvesWith('ok2')]; const result = await safeAllSettled(tasks); assert(result.succeeded.length === 2); assert(result.failed.length === 1);
SE-05: ReDoS in Glob-to-Regex
Symptom: Glob pattern matching hangs or takes exponential time on long path strings.
Root cause: Naive glob-to-regex conversion produces patterns with nested quantifiers like (.) or (.+)+ which backtrack exponentially on non-matching strings.
Fix:
// Use anchored, non-backtracking patterns // BAD: (.) -> exponential backtracking // GOOD: [^/]* -> linear, no backtracking function globToRegex(glob) { const escaped = glob.replace(/[.+^${}()|[]\]/g, '\$&'); return escaped .replace(/**/g, 'DOUBLE') .replace(/*/g, '[^/]') // Single * -> no path separators .replace(/DOUBLE/g, '.'); // ** -> any path }
Test assertion:
const regex = globToRegex('**/*.js'); const longString = 'a'.repeat(10000) + '.ts'; const start = Date.now(); regex.test(longString); assert(Date.now() - start < 100); // Must complete in <100ms
SE-06: DST Arithmetic Bugs
Symptom: Date calculations are off by 1 hour for events that cross daylight saving time boundaries.
Root cause: Adding 24 60 60 * 1000ms to a timestamp does not always equal "tomorrow" — DST transitions can make a day 23 or 25 hours long.
Fix:
// WRONG: assumes 24h = 1 day const tomorrow = new Date(date.getTime() + 86400000);
// CORRECT: use date arithmetic, not ms arithmetic const tomorrow = new Date(date); tomorrow.setDate(tomorrow.getDate() + 1);
Test assertion:
// Test across a known DST boundary (e.g., March DST change) const dst = new Date('2026-03-08T01:00:00'); // Day before US spring forward const nextDay = addDays(dst, 1); assert(nextDay.getDate() === 9); assert(nextDay.getHours() === 1); // Same time, next day
SE-07: Array Mutation During forEach Iteration
Symptom: Items are skipped or processed twice; behavior is non-deterministic.
Root cause: Mutating an array (push, splice, shift) while iterating with forEach or for...of causes the iterator to skip or re-visit elements.
Fix:
// WRONG: mutates during iteration arr.forEach(item => { if (condition) arr.push(newItem); // Skips items });
// CORRECT: collect mutations, apply after const toAdd = []; arr.forEach(item => { if (condition) toAdd.push(newItem); }); arr.push(...toAdd);
// OR: work on a copy [...arr].forEach(item => { ... });
Test assertion:
const arr = [1, 2, 3]; const result = safeForEach(arr, item => item * 2); assert(result.length === 3); // Original not mutated, all items processed
Usage
Invoke at the START of any debugging session:
Skill({ skill: 'sharp-edges' });
Then match your symptom against SE-01 through SE-07. If your bug is new and reproducible, add it to this catalogue.
Adding New Entries
Pattern for new entries:
SE-0N: [Short Title]
Symptom: [What you observe] Root cause: [Why it happens] Fix: [Code fix with before/after] Test assertion: [Minimal test that would catch it] Files affected: [Where in agent-studio this applies]
Memory Protocol
After invoking: if you find a new sharp edge, append it here via technical-writer agent.
Memory Protocol (MANDATORY)
Before starting: Read .claude/context/memory/learnings.md
After completing:
-
New pattern -> .claude/context/memory/learnings.md
-
Issue found -> .claude/context/memory/issues.md
-
Decision made -> .claude/context/memory/decisions.md
ASSUME INTERRUPTION: If it's not in memory, it didn't happen.