Harden input hygiene (3.9.15) - #14
Conversation
Strip terminal control characters on write and read, refuse mostly-nonprintable debrief input including stdin, and fail loudly when .fde is a file instead of a directory. Co-authored-by: Cursor <cursoragent@cursor.com>
| `engagement path is not a directory (memory missing/broken): ${p}\n` + | ||
| ' repair: remove that file, then re-run: fde resume --init <name>' | ||
| console.error(msg) | ||
| if (opts.forWrite) process.exit(1) |
There was a problem hiding this comment.
🟡 Silent session-snapshot commands abort loudly when engagement memory is a stray file
The pre-compaction and session-end snapshot commands are halted (process.exit(1) in acceptEngagementPath at bin/fde.js:180) before they can quietly give up, so an engagement folder that has been replaced by a file makes these otherwise-silent background commands print an error and exit with failure instead of doing nothing.
Impact: On a broken engagement, background snapshot steps that are supposed to stay invisible now fail loudly, contradicting their documented never-break-the-session promise.
How the forWrite exit bypasses the graceful no-op paths
acceptEngagementPath (bin/fde.js:171-186) calls process.exit(1) (and failFs, which also exits) whenever opts.forWrite is true and the path exists but is not a directory. This runs inside resolveEngagement({ forWrite: true }).
cmdPreserve (bin/fde.js:1445-1472) wraps its whole body in try/catch and, before this change, returned silently for a file-.fde because fs.existsSync(path.join(eng, 'context.md')) was false (bin/fde.js:1448). Now resolveEngagement({ forWrite: true }) calls process.exit(1), which is not catchable by the try/catch, so the previously-silent no-op becomes a loud failure.
cmdCapture (bin/fde.js:1413-1415) is likewise designed to exit 0 silently (if (!eng) process.exit(0)), and the comment at bin/fde.js:256-257 states "session capture must never crash a hook."
Note the shipped hooks (hooks/session-stop:69-72, hooks/pre-compact:83-85) invoke these with >/dev/null 2>&1 || true, which suppresses stderr and ignores the exit code, so the shipped-hook impact is masked; direct CLI invocations or integrations that do not mask output would surface the regression.
Prompt for agents
acceptEngagementPath (bin/fde.js:171-186) calls process.exit(1) (and failFs) when opts.forWrite is set and the resolved .fde path is a file rather than a directory. This is reached via resolveEngagement({ forWrite: true }), which is called by cmdCapture (bin/fde.js:1414) and cmdPreserve (bin/fde.js:1447) - both are explicitly designed to never break a session (cmdCapture exits 0 silently on no engagement; cmdPreserve wraps its body in try/catch and returns silently). Because process.exit is not catchable, these silent background commands now abort loudly on a broken engagement (a file named .fde). Consider adding a soft mode to acceptEngagementPath/resolveEngagement (analogous to the existing opts.soft pattern used by refuseSymlinkWrite/withFileLock/lockedAppendFile) so that capture/preserve resolve to null instead of exiting, while interactive write commands (log/debrief/owner) keep the loud refuse. Ensure the graceful no-op behavior of preserve/capture on a broken .fde is preserved.
Was this helpful? React with 👍 or 👎 to provide feedback.
Strip terminal control characters on write and read, refuse mostly-nonprintable debrief input including stdin, and fail loudly when .fde is a file instead of a directory.
Summary
readClean— kills ANSI trust-color smuggling.fdeas a file: loud refuse, no fake green TRIAGE; clearer ENOTDIR messageTest plan
npm run check(new regressions for ANSI, binary stdin,.fde-as-file)v3.9.15→npm publishMade with Cursor