sharp-edges

v2026.09.24

Living catalogue of 7 known hazard entries (SE-01 through SE-07) specific to agent-studio: Windows backslash paths, prototype pollution, hook exit codes, async swallowing, ReDoS in glob-to-regex, DST arithmetic, array mutation during iteration. Each entry: symptom, root cause, fix, test assertion.

GitHub
Install command
npx skhub add oimiragieo/sharp-edges
Markdown
SKILL.md

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.

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

Not specified

Source path

.claude/skills/sharp-edges

Default branch

main

Latest commit

64b580e

Tree SHA

42a1df4