[harness] Task-row parser has no passthrough — claim/TaskUpdate silently strips any non-schema key (origin, created_utc, session_id, ...)
Summary
The task-row reader (task-store file parsing) validates each ~/.claude/tasks/<sid>/<id>.json row through a Zod object schema with no .passthrough(). Any key not declared on that schema — including workspace-added fields such as origin, created_utc, and session_id — is silently dropped the moment the row is re-serialized, which happens on:
- Agent-team self-claim (
claimTask), which writes{...parsedTask, owner: r, id}from the already-parsed (and therefore already-stripped) object back over the row file. - TaskUpdate, which writes
{...c, ...r, id: n}from the same parsed object.
Confirmed by disassembling the installed CLI (strings on the shipped bundle, version 2.1.282): the reader (le(e,n)) calls Oe().safeParse(r) and returns i.data; Oe is d({id, subject, description, activeForm?, owner?, status:<enum>, blocks, blockedBy, metadata?}) — a plain Zod object, not .passthrough(). Any caller that layers extra fields onto a task row for its own bookkeeping loses them on the very first claim or update, with no warning and no error — the write succeeds silently.
Relationship to existing issues
This looks like the underlying mechanism behind two issues already filed against this behavior, neither of which names key-stripping specifically:
- #93668 — self-claim has no role/tool filter. The stripping described here happens on the same self-claim write path (
Le(e,{owner:r},n)), and is a second, independent defect on that path: even a correct claim destroys caller-added fields. - #97903 — TaskUpdate reports success while dropping the metadata merge or applying only one field under contention. The same non-passthrough parse-then-spread pattern (
{...c, ...r, id: n}) is consistent with, and may be the root cause of, the drop described there.
Repro
- Mint a task row with an extra top-level key not in the schema, e.g.
{"id":"t1","subject":"x","description":"x","status":"pending","blocks":[],"blockedBy":[],"origin":"root","created_utc":"2026-01-01T00:00:00Z"}. - Have a teammate self-claim it, or issue a
TaskUpdateagainst it. - Read the row file back:
originandcreated_utcare gone. Only the declared schema keys (plus whatever the claim/update itself set) remain.
Impact
Any downstream tooling that relies on a task row carrying more information than the harness's own schema (audit trails, provenance, lease bookkeeping, anything keyed off "who/when minted this row") loses that data non-deterministically — as soon as any claim or update touches the row, not on a predictable schedule. Because metadata (an untyped object) is one of the declared keys, it is the only place extra data currently survives a claim/update round-trip — which is a workable convention downstream, but isn't documented anywhere, and every other top-level key is at risk.
Suggested fix
Either:
- Add
.passthrough()to the row-parsing schema (Oe) sosafeParsepreserves unknown keys, or - Explicitly merge
{...raw, ...parsedAndMutatedFields}on write (raw-then-parsed, not parsed-then-spread), so a write only ever changes the fields the operation intends to change.
Happy to provide the disassembly excerpt in more detail if useful; I did not attempt to patch the shipped binary and instead worked around it entirely in userspace (mirroring the at-risk fields into metadata at mint time, then restoring them from there after any downstream claim/update), which is the only currently-safe pattern.