Problem
SYN002–SYN005 catch specific capability-model bypasses: native throws, console.* output, eval/Function(), and process.env access. But none of them catch process.exit().
A fn that calls process.exit() or process.exit(code):
- Terminates the entire host process — not just the fn, not just the bot, but the runtime
- Produces no return value and never runs caller code after it
- Completely bypasses botscript's
Result<T, E> error contract — callers relying on ?, match, or throws {} propagation will never see this
- Has no capability declaration, no
throws {}, nothing in the fn header to signal the kill
- Leaves the calling chain with no evidence anything went wrong — no error, no diagnostic, just silence
It is the most severe silent-exit pattern botscript currently misses.
Real risk patterns
Code-generation models commonly produce process.exit() for:
- CLI-style guard clauses:
if (!cfg.valid) process.exit(1)
- Error handlers that want to "bail out":
catch (e) { process.exit(1) }
- Config-load failures:
if (!env) { console.error(...); process.exit(1) }
None of these are caught by any current diagnostic.
Proposed detection
Extends passSynCheck following the SYN005 (process.env) pattern exactly:
Fires on:
process.exit(...) — token process not preceded by . or ?., followed by ./?., then exit, then (
process?.exit(...) — optional chain form (same risk, should be flagged)
Suppressed by:
unsafe "reason" { ... } blocks
unsafe "reason" fn bodies
Not fired for:
obj.process.exit(...) — process preceded by ./?. (member access on a local named process)
Severity: warning (non-blocking), same as SYN002–SYN005. There are edge cases where a toplevel bootstrap script genuinely needs process.exit, and unsafe "exits on invalid config" { process.exit(1) } is the right escape.
Implementation sketch
Detect immediately after the process.env detection block in passSynCheck:
// --- process.exit(...) detection ---
// Look for: `process` not preceded by `.`/`?.`, followed by `.`/`?.`, then `exit`, then `(`
if (tok.text === "process") {
const prevProcIdx = prevSignificant(tokens, i - 1);
const prevProc = tokens[prevProcIdx];
if (prevProc && ((prevProc.kind === "punct" && prevProc.text === ".") || prevProc.kind === "questionDot"))
continue;
const nextDotIdx = nextSignificant(tokens, i + 1);
const nextDot = tokens[nextDotIdx];
const isDot = nextDot && nextDot.kind === "punct" && nextDot.text === ".";
const isOptDot = nextDot && nextDot.kind === "questionDot";
if (!isDot && !isOptDot) continue;
const memberIdx = nextSignificant(tokens, nextDotIdx + 1);
const memberTok = tokens[memberIdx];
if (!memberTok || memberTok.kind !== "ident" || memberTok.text !== "exit") continue;
const parenIdx = nextSignificant(tokens, memberIdx + 1);
const parenTok = tokens[parenIdx];
if (!parenTok || !(parenTok.kind === "open" && parenTok.text === "(")) continue;
if (isInsideRange(tok.start, unsafeRanges)) continue;
// emit SYN006
}
Relation to SYN002–SYN005
| Code |
Pattern |
Impact |
| SYN002 |
throw |
Bypasses Result contract |
| SYN003 |
console.* |
Bypasses stdout/stderr capability |
| SYN004 |
eval/Function() |
Bypasses all static checks at once |
| SYN005 |
process.env |
Hides deployment dependencies |
| SYN006 |
process.exit() |
Terminates host process; bypasses Result, throws, all recovery logic |
Note on timing
Should be implemented after PR #145 (SYN004) and PR #143 (SYN005) merge, since the next available code on main is SYN004 — implementing before those PRs land would create a conflicting code assignment.
Problem
SYN002–SYN005 catch specific capability-model bypasses: native throws, console.* output, eval/Function(), and process.env access. But none of them catch process.exit().
A fn that calls
process.exit()orprocess.exit(code):Result<T, E>error contract — callers relying on?,match, orthrows {}propagation will never see thisthrows {}, nothing in the fn header to signal the killIt is the most severe silent-exit pattern botscript currently misses.
Real risk patterns
Code-generation models commonly produce
process.exit()for:if (!cfg.valid) process.exit(1)catch (e) { process.exit(1) }if (!env) { console.error(...); process.exit(1) }None of these are caught by any current diagnostic.
Proposed detection
Extends
passSynCheckfollowing the SYN005 (process.env) pattern exactly:Fires on:
process.exit(...)— tokenprocessnot preceded by.or?., followed by./?., thenexit, then(process?.exit(...)— optional chain form (same risk, should be flagged)Suppressed by:
unsafe "reason" { ... }blocksunsafe "reason" fnbodiesNot fired for:
obj.process.exit(...)—processpreceded by./?.(member access on a local namedprocess)Severity: warning (non-blocking), same as SYN002–SYN005. There are edge cases where a toplevel bootstrap script genuinely needs
process.exit, andunsafe "exits on invalid config" { process.exit(1) }is the right escape.Implementation sketch
Detect immediately after the
process.envdetection block inpassSynCheck:Relation to SYN002–SYN005
throwconsole.*eval/Function()process.envprocess.exit()Note on timing
Should be implemented after PR #145 (SYN004) and PR #143 (SYN005) merge, since the next available code on main is SYN004 — implementing before those PRs land would create a conflicting code assignment.