Filed unassigned from the #16095 door enumeration (branch claude/issue-16095-hook-rules-reach-handler-authored-hooks). Measured on origin/main @ 5d55afec4d through the real CLI (packages/cli/test/lint-hook-rules-reach-handler-hooks.e2e.test.ts on that branch pins the reading).
The reading
Every rule in the hook-body-* / hook-api-update-readonly-* family opens on body.language === 'js'. Whether a hook authored as handler: async (ctx) => { … } reaches it is a property of what each command hands the rule registry:
| door |
lowers inline handlers before the rules run? |
handler-authored readonly write via ctx.api |
os build |
yes — lowerCallables runs before ObjectStackDefinitionSchema.safeParse (compile.ts) |
refused, hook-api-update-readonly-field, exit 1 |
os lint |
since #16095 — lintConfig hands the parsed tier the lowered view |
refused, exit 1 |
os validate |
no — validate.ts parses normalized directly |
passes, exit 0, no finding |
Control on the same fixture: the identical statement authored as an explicit body: { language: 'js', source } is refused by os validate (exit 1, same rule). So the silence is the door, not the rule.
This is the #4409 / #3782 class ("one command returns clean for a stack another refuses"), on the hook surface: the reference-integrity suite is gating and registered for all three commands, but os validate gives it an input on which the hook members cannot fire.
Why it was not folded into #16095
#16095's ask and ruling were scoped to os lint; making os validate lower would change what os validate refuses (a stack that validates green today would exit 1), which is its own accept/reject decision. Also note os validate runs lintUnknownStackKeys / lintUnknownAuthoringKeys pre-parse on the same normalized stack; a lowering pass would have to sit between those and the parse, mirroring compile.ts step 2b.
Suggested shape
Run lowerCallables(normalized) in validate.ts before the parse and parse lowering.lowered, exactly as compile.ts does — the same call, not a copy — and hand the registry parsed: result.data. The card's control fixture is the red-first test.
Blast radius on this tree: zero on the ten objectstack.config.ts corpus files (none of the repo's handler-authored hooks writes through ctx.api; the two that exist stamp ctx.input). Outside this tree the affected population is the one #16095 measured: 39 of 39 hooks in the reference CRM are handler-authored.
Related: #16095 (lint half, in flight) · #3782 (closed — the same class on flows) · #13838 (open — os build acceptance, deliberately untouched).
Filed unassigned from the #16095 door enumeration (branch
claude/issue-16095-hook-rules-reach-handler-authored-hooks). Measured onorigin/main@5d55afec4dthrough the real CLI (packages/cli/test/lint-hook-rules-reach-handler-hooks.e2e.test.tson that branch pins the reading).The reading
Every rule in the
hook-body-*/hook-api-update-readonly-*family opens onbody.language === 'js'. Whether a hook authored ashandler: async (ctx) => { … }reaches it is a property of what each command hands the rule registry:ctx.apios buildlowerCallablesruns beforeObjectStackDefinitionSchema.safeParse(compile.ts)hook-api-update-readonly-field, exit 1os lintlintConfighands theparsedtier the lowered viewos validatevalidate.tsparsesnormalizeddirectlyControl on the same fixture: the identical statement authored as an explicit
body: { language: 'js', source }is refused byos validate(exit 1, same rule). So the silence is the door, not the rule.This is the #4409 / #3782 class ("one command returns clean for a stack another refuses"), on the hook surface: the reference-integrity suite is
gatingand registered for all three commands, butos validategives it an input on which the hook members cannot fire.Why it was not folded into #16095
#16095's ask and ruling were scoped to
os lint; makingos validatelower would change whatos validaterefuses (a stack that validates green today would exit 1), which is its own accept/reject decision. Also noteos validaterunslintUnknownStackKeys/lintUnknownAuthoringKeyspre-parse on the same normalized stack; a lowering pass would have to sit between those and the parse, mirroringcompile.tsstep 2b.Suggested shape
Run
lowerCallables(normalized)invalidate.tsbefore the parse and parselowering.lowered, exactly ascompile.tsdoes — the same call, not a copy — and hand the registryparsed: result.data. The card's control fixture is the red-first test.Blast radius on this tree: zero on the ten
objectstack.config.tscorpus files (none of the repo's handler-authored hooks writes throughctx.api; the two that exist stampctx.input). Outside this tree the affected population is the one #16095 measured: 39 of 39 hooks in the reference CRM are handler-authored.Related: #16095 (lint half, in flight) · #3782 (closed — the same class on flows) · #13838 (open —
os buildacceptance, deliberately untouched).