Summary
no-unsafe-catch-error-property only tracks unsafe .message/.stack/.code/.status/.cause/.name access on the catch clause's own parameter identifier (top.varName, set once in the CatchClause visitor). It never follows a simple rename alias (const err = error;) created inside the catch block, so any unsafe property access performed through the alias escapes detection entirely.
Its sibling rule, no-caught-error-interpolation, already solves this for bare-identifier interpolation: isCaughtErrorIdentifier (eslint-factory/src/rules/no-caught-error-interpolation.ts:145-160) explicitly resolves one hop through a const alias = caughtVar; declarator back to the original catch/rejection-handler binding before deciding whether an identifier is a caught error. no-unsafe-catch-error-property has no equivalent resolution step — its MemberExpression visitor (eslint-factory/src/rules/no-unsafe-catch-error-property.ts:268-289) compares obj.name directly against the catch param's own name and nothing else.
Live grounding (3 sites, same PR, 2 files)
All three are new/changed in actions/setup/js/codex_harness.cjs (part of today's Codex engine fix):
// codex_harness.cjs:327-332
} catch (error) {
const err = /** @type {Error} */ error;
// ...
throw new Error(`--prompt-file '${promptFile}' is not readable: ${err.message}`, { cause: err });
}
// codex_harness.cjs:570-574
} catch (error) {
const err = /** @type {Error} */ error;
log(`warning: unable to configure provider endpoint from /reflect: ${err.message}`);
return { env, configured: false };
}
// codex_harness.cjs:732-736
} catch (err) {
const e = /** @type {Error} */ err;
log(`fatal: ${e.message}`);
process.exit(1);
}
In every case the catch binding (error/err) is renamed via a const declarator carrying only a JSDoc @type cast comment (no runtime narrowing — error/err could still be a non-Error throw), then .message is read off the alias, not the original binding. The rule's MemberExpression visitor never sees these because obj.name is err/e, not the tracked varName (error/err).
Confirmed via the rule's own test file (no-unsafe-catch-error-property.test.ts:99) that a fresh const e = new Error() is correctly treated as valid (it isn't derived from the catch binding) — but there is no test for a const e = <catchParam>; rename, which is the actual live pattern above.
Suggested fix
Port the one-hop alias resolution already used by no-caught-error-interpolation (isCaughtErrorIdentifier, lines 145-160): when a CatchClause is entered, additionally register any const <alias> = <catchParam>; declarator found in the same block as a second tracked name (or resolve MemberExpression.object through the scope chain back to the catch param) before deciding whether .message/.stack/etc. access is unsafe.
Acceptance criteria
Generated by 🤖 ESLint Refiner · claude · agent · 364.6 AIC · ⌖ 6.94 AIC · ⊞ 5.2K · ◷
Summary
no-unsafe-catch-error-propertyonly tracks unsafe.message/.stack/.code/.status/.cause/.nameaccess on the catch clause's own parameter identifier (top.varName, set once in theCatchClausevisitor). It never follows a simple rename alias (const err = error;) created inside the catch block, so any unsafe property access performed through the alias escapes detection entirely.Its sibling rule,
no-caught-error-interpolation, already solves this for bare-identifier interpolation:isCaughtErrorIdentifier(eslint-factory/src/rules/no-caught-error-interpolation.ts:145-160) explicitly resolves one hop through aconst alias = caughtVar;declarator back to the original catch/rejection-handler binding before deciding whether an identifier is a caught error.no-unsafe-catch-error-propertyhas no equivalent resolution step — itsMemberExpressionvisitor (eslint-factory/src/rules/no-unsafe-catch-error-property.ts:268-289) comparesobj.namedirectly against the catch param's own name and nothing else.Live grounding (3 sites, same PR, 2 files)
All three are new/changed in
actions/setup/js/codex_harness.cjs(part of today's Codex engine fix):In every case the catch binding (
error/err) is renamed via aconstdeclarator carrying only a JSDoc@typecast comment (no runtime narrowing —error/errcould still be a non-Errorthrow), then.messageis read off the alias, not the original binding. The rule'sMemberExpressionvisitor never sees these becauseobj.nameiserr/e, not the trackedvarName(error/err).Confirmed via the rule's own test file (
no-unsafe-catch-error-property.test.ts:99) that a freshconst e = new Error()is correctly treated as valid (it isn't derived from the catch binding) — but there is no test for aconst e = <catchParam>;rename, which is the actual live pattern above.Suggested fix
Port the one-hop alias resolution already used by
no-caught-error-interpolation(isCaughtErrorIdentifier, lines 145-160): when aCatchClauseis entered, additionally register anyconst <alias> = <catchParam>;declarator found in the same block as a second tracked name (or resolveMemberExpression.objectthrough the scope chain back to the catch param) before deciding whether.message/.stack/etc. access is unsafe.Acceptance criteria
catch (error) { const err = error; use(err.message); }(no guard) is flagged, witherrreported as the unsafe variable.catch (err) { const e = new Error(); console.log(e.message); }remains valid (freshError, not an alias of the catch binding, must not regress).const err = error; if (err instanceof Error) { err.message }) remain valid — alias resolution must feed into the existing guard logic (isGuardedByAncestorBranch/isCallOrderingGuarded/hasPriorEarlyExitInstanceofGuard), not just suppress/report blindly.no-unsafe-catch-error-property.test.tscovering the alias-rename shape (both flagged and guarded).