Found while fixing #16328 (the manifest permissions block's unknown-key refusal). Filed unassigned and unlabelled for triage. Not fixed there — that card owns kernel/manifest.zod.ts only, and this is a different file reached from four other metadata types.
What was measured
Against packages/spec built from origin/main at f89812e4d, through dist/ai/index.mjs and dist/shared/index.mjs:
AgentSchema.safeParse({ name: 'a', protection: { lockk: 'system' } })
RAW code=unrecognized_keys path=["protection"] :: Unrecognized key: "lockk"
FORMATTED (formatZodError):
✗ protection: Unrecognized key: "lockk"
lockk is one keystroke from the declared lock. The author is told the key is wrong and nothing else: no surface name, no declared-key list, and no rename suggestion.
Why this is a defect and not a preference
ProtectionSchema (packages/spec/src/shared/protection.zod.ts:63) is a bare z.object({ ... }).strict() with no error map. It is closed — the refusal is loud, nothing is silently stripped — but it never adopted the strictObject helper that the #4001 unknown-key campaign made the standard for authorable shapes, so it emits zod's own default text instead of the campaign's named message.
Every neighbouring authorable block reached from the same schemas names all three parts. Compare, measured in the same run against the manifest:
Unrecognized key(s) on the `engines` block of this package manifest: `protocl`.
Did you mean `protocl` -> `protocol`? ...
Reach
protection is declared on four schemas, three of which are registered metadata types an author writes by hand:
ai/agent.zod.ts:374 (agent)
ai/tool.zod.ts:204 (tool)
ai/skill.zod.ts (skill)
identity/position.zod.ts:127
So defineStack({ agents }), the metadata REST doors and the Studio forms all reach this refusal.
Note on the strictness ledger
docs/audits/2026-07-unknown-key-strictness-ledger.md has no row for shared/protection.zod.ts. Its one near-mention (line 501) is about a counting bug and names kernel/metadata-protection.zod.ts — a different file. So this site appears not to have been enumerated by the campaign rather than deliberately accepted; that is worth confirming at triage before treating it as a plain fix.
Suggested direction
Convert ProtectionSchema to strictObject({ surface, history }, { ... }), exactly as #16328 did for PluginPermissionsSchema. That change cannot move the accept set: strictObject is z.object(shape, { error }).strict(), and an error map is consulted only once an issue is already being raised.
⚠️ The population is not established here. This is one site found while working a neighbouring card, not a sweep. Whether other closed authorable shapes still carry zod's bare message needs its own measured count — #14722's triage already ruled that a sweep needs its own card with a measured population, so this card deliberately does not perform one.
Generated by Claude Code
Found while fixing #16328 (the manifest
permissionsblock's unknown-key refusal). Filed unassigned and unlabelled for triage. Not fixed there — that card ownskernel/manifest.zod.tsonly, and this is a different file reached from four other metadata types.What was measured
Against
packages/specbuilt fromorigin/mainatf89812e4d, throughdist/ai/index.mjsanddist/shared/index.mjs:lockkis one keystroke from the declaredlock. The author is told the key is wrong and nothing else: no surface name, no declared-key list, and no rename suggestion.Why this is a defect and not a preference
ProtectionSchema(packages/spec/src/shared/protection.zod.ts:63) is a barez.object({ ... }).strict()with no error map. It is closed — the refusal is loud, nothing is silently stripped — but it never adopted thestrictObjecthelper that the #4001 unknown-key campaign made the standard for authorable shapes, so it emits zod's own default text instead of the campaign's named message.Every neighbouring authorable block reached from the same schemas names all three parts. Compare, measured in the same run against the manifest:
Reach
protectionis declared on four schemas, three of which are registered metadata types an author writes by hand:ai/agent.zod.ts:374(agent)ai/tool.zod.ts:204(tool)ai/skill.zod.ts(skill)identity/position.zod.ts:127So
defineStack({ agents }), the metadata REST doors and the Studio forms all reach this refusal.Note on the strictness ledger
docs/audits/2026-07-unknown-key-strictness-ledger.mdhas no row forshared/protection.zod.ts. Its one near-mention (line 501) is about a counting bug and nameskernel/metadata-protection.zod.ts— a different file. So this site appears not to have been enumerated by the campaign rather than deliberately accepted; that is worth confirming at triage before treating it as a plain fix.Suggested direction
Convert
ProtectionSchematostrictObject({ surface, history }, { ... }), exactly as #16328 did forPluginPermissionsSchema. That change cannot move the accept set:strictObjectisz.object(shape, { error }).strict(), and an error map is consulted only once an issue is already being raised.Generated by Claude Code