# Sharp Edges

> 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.

- Skill: `oimiragieo/sharp-edges` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add oimiragieo/sharp-edges`
- Raw SKILL.md: https://api.skillmd.com/api/skills/oimiragieo/sharp-edges/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: oimiragieo (https://skillmd.com/u/oimiragieo)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/oimiragieo/sharp-edges

---


# 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:**

```javascript
// ALWAYS normalize before regex matching
const rel = path.relative(root, filePath).replace(/\\/g, '/');
```

**Test assertion:**

```javascript
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:**

```javascript
// Use safeParseJSON — strips __proto__, constructor, prototype keys
const { safeParseJSON } = require('.claude/lib/utils/safe-json.cjs');
const { success, data } = safeParseJSON(rawInput, {});
```

**Test assertion:**

```javascript
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:**

```javascript
// 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:**

```javascript
// 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:**

```javascript
// 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:**

```javascript
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:**

```javascript
// 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:**

```javascript
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:**

```javascript
// 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:**

```javascript
// 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:**

```javascript
// 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:**

```javascript
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:

```javascript
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.

