Found while wiring the action-surface readonly rule (#13770). Not folded into that PR — it is a different rule's author-facing text, and #13770's scope fence forbids touching the hook rule's behaviour.
The defect
Two shipped rules tell an author how to make a readonlyWhen write land. Both remedies are measured false on origin/main.
packages/lint/src/validate-readonly-hook-writes.ts, the hook-api-update-readonly-when-field hint:
readonlyWhen strips even a beforeUpdate-derived value, so an own-hook stamp is NOT a workaround here. If automation must maintain 'FIELD' regardless of record state, write it through ctx.api.sudo().
packages/lint/src/validate-readonly-flow-writes.ts, the flow-update-readonly-when-field hint:
If automation must maintain this field regardless of record state, run the flow runAs:'system'.
Both sentences say elevation is the way out. It is not, and the repo already pins the opposite:
engine-readonly-when-derived-writes.test.ts, "LOCK 2 — isSystem does NOT exempt a caller-supplied value" — an { context: { isSystem: true } } update to a field whose readonlyWhen predicate is TRUE is still stripped.engine-readonly-strict-writes.test.ts, "covers readonlyWhen too — the arm a trusted (isSystem) caller can still hit" — same fact from the strict-mode side.
Measured directly on origin/main (real ObjectQL engine over a memory driver, field declared readonlyWhen: record.status == 'paid', record in the locked state):
[ctx.api, non-system] readonlyWhen -> stored value unchanged (stripped)
[ctx.api.sudo()] readonlyWhen -> stored value unchanged (stripped)
The second half of the hook hint is stale in the other direction. It asserts that readonlyWhen strips a beforeUpdate-derived value, which is exactly the behaviour #9107 removed: engine-readonly-when-derived-writes.test.ts opens with "THE REPORT: a hook-derived value on a TRUE readonlyWhen field now LANDS". So the hint rules out the remedy that works and points at one that does not.
Why it is worth fixing rather than leaving
The hint is the whole product of an advisory rule — the finding does not block anything, so the sentence is all the author acts on. Following it costs a round trip: the author elevates, re-runs, sees the same missing column, and now has an unnecessary sudo() in the tree with no compensating behaviour. sudo bypasses the acting user's row and field permissions, so this is advice to widen a write's privileges for no effect.
The correct remedies, as measured
Suggested scope
Reword the two hints and pin the corrected text, the way validate-readonly-action-writes.test.ts pins its own (does NOT offer elevation as the remedy). Rule ids, severities and match sets are untouched — this is message text only.
Generated by Claude Code
Generated by Claude Code
Found while wiring the action-surface readonly rule (#13770). Not folded into that PR — it is a different rule's author-facing text, and #13770's scope fence forbids touching the hook rule's behaviour.
The defect
Two shipped rules tell an author how to make a
readonlyWhenwrite land. Both remedies are measured false onorigin/main.packages/lint/src/validate-readonly-hook-writes.ts, thehook-api-update-readonly-when-fieldhint:packages/lint/src/validate-readonly-flow-writes.ts, theflow-update-readonly-when-fieldhint:Both sentences say elevation is the way out. It is not, and the repo already pins the opposite:
engine-readonly-when-derived-writes.test.ts, "LOCK 2 — isSystem does NOT exempt a caller-supplied value" — an{ context: { isSystem: true } }update to a field whosereadonlyWhenpredicate is TRUE is still stripped.engine-readonly-strict-writes.test.ts, "covers readonlyWhen too — the arm a trusted (isSystem) caller can still hit" — same fact from the strict-mode side.Measured directly on
origin/main(real ObjectQL engine over a memory driver, field declaredreadonlyWhen: record.status == 'paid', record in the locked state):The second half of the hook hint is stale in the other direction. It asserts that
readonlyWhenstrips abeforeUpdate-derived value, which is exactly the behaviour #9107 removed:engine-readonly-when-derived-writes.test.tsopens with "THE REPORT: a hook-derived value on a TRUE readonlyWhen field now LANDS". So the hint rules out the remedy that works and points at one that does not.Why it is worth fixing rather than leaving
The hint is the whole product of an advisory rule — the finding does not block anything, so the sentence is all the author acts on. Following it costs a round trip: the author elevates, re-runs, sees the same missing column, and now has an unnecessary
sudo()in the tree with no compensating behaviour.sudobypasses the acting user's row and field permissions, so this is advice to widen a write's privileges for no effect.The correct remedies, as measured
readonlyWhenpredicate is FALSE.beforeUpdatehook on the target object: a hook-written value is not caller-supplied, and since [objectql] TRUE readonlyWhen strips beforeUpdate-derived values too — a conditionally-locked derived field has no server-side write path at all (isSystem included), unlike static readonly's hook-stamp protection #9107 it lands even on a locked record.ctx.api.sudo(),runAs: 'system') changes nothing for this shape and should not be offered.Suggested scope
Reword the two hints and pin the corrected text, the way
validate-readonly-action-writes.test.tspins its own (does NOT offer elevation as the remedy). Rule ids, severities and match sets are untouched — this is message text only.Generated by Claude Code
Generated by Claude Code