[harness] Task-row parser has no passthrough — claim/TaskUpdate silently strips any non-schema key (origin, created_utc, session_id, ...)

Status Open
Reported on v2.1.282
Maintainer reply None cached
Activity 0 comments · opened Sep 29, 2026

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

  1. 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"}.
  2. Have a teammate self-claim it, or issue a TaskUpdate against it.
  3. Read the row file back: origin and created_utc are 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) so safeParse preserves 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.

View original on GitHub ↗