{"owner":"colinhacks","repo":"zod","hasSkills":true,"totalSkillsCount":17,"totalTokensCount":17476,"categories":["anthropic-skill","root-instruction","claude-rule","plugin-manifest","cursor-rule"],"hasMcp":false,"mcpConfig":null,"found":[".claude/skills/security-advisory/SKILL.md",".claude/skills/triage/SKILL.md","AGENTS.md","CLAUDE.md","packages/bench/typia-case/README.md","packages/docs/.vscode/settings.json","packages/docs/README.md","packages/docs/content/meta.json","packages/docs/package.json","packages/docs/tsconfig.json","packages/docs/tsconfig.test.json","packages/docs/vercel.json","packages/mini/README.md","packages/resolution/README.md","packages/tsc/README.md","packages/zod/README.md",".cursorrules (typescript-zod-tailwind-nextjs-cursorrules-prompt-.mdc)"],"skills":{".claude/skills/security-advisory/SKILL.md":"---\nname: security-advisory\ndescription: Triage a draft security advisory in colinhacks/zod — a private vulnerability report from the Security tab, named by GHSA id (GHSA-xxxx-xxxx-xxxx) or reached from \"the draft advisories\", \"the security reports\", \"the vulnerability queue\". Use instead of the `triage` skill whenever the ticket is an advisory rather than a public issue or PR, because the workflow is different: the report is private, the burden of proof is on the reporter, the fix must be LANDED before any comment is drafted, and the publish-or-decline call has its own calculus. Covers reproducing a PoC against main, the schema-vs-input test, deduplicating a cluster of reports into distinct defects, landing the fix first, the publish-vs-decline framework, per-reporter privacy rules that make a GHSA id unlinkable, and the results.md format under .triage/advisories/GHSA-xxxx-xxxx-xxxx/.\n---\n\n# Triaging a draft security advisory\n\nAdvisories are not issues with a scarier label. Four things differ, and each one changes the order you work in:\n\n- **The report is private**, and private in both directions — see `## Privacy cuts both ways`.\n- **The burden of proof is on the reporter.** Most submissions are scanner output or a rediscovery of intended behavior. A plausible writeup is not evidence.\n- **The fix lands before the comment exists.** See `## Land the fix first`. This is the rule most often broken and the one that wastes the most of Colin's time.\n- **Real defect and publishable vulnerability are different questions.** See `## Publish or decline`.\n\nEverything in the `triage` skill about where files live, `results.md` frontmatter, close-comment voice, and reporting back in chat still applies. This skill replaces only its `## Investigating a draft advisory` section.\n\n**Never accept, publish, decline, or comment on an advisory.** The write-up and the drafted note are the deliverables; the disposition is Colin's. Landing a fix is a code change and follows the repo's normal rules; publishing a CVE is not, and is never yours.\n\n## Reading the queue\n\n```bash\ngh api repos/colinhacks/zod/security-advisories --paginate \\\n  --jq '.[] | select(.state==\"triage\") | [.ghsa_id, .severity, .summary] | @tsv'\ngh api repos/colinhacks/zod/security-advisories/<GHSA-ID> --jq '.description'   # full report + PoC\n```\n\nWrite-ups go to `.triage/advisories/<GHSA-ID>/results.md`, scratch files beside them. Frontmatter swaps `number:` for `ghsa:` and adds `severity_claimed:` (what the reporter asserted) next to `severity_actual:` (yours, or `none`).\n\n`verdict` is one of `valid`, `not-a-vulnerability`, `already-fixed`, `duplicate` (with `duplicate_of:`), `needs-reporter-info`, `needs-maintainer-decision`. Add `fixed_by:` / `fix_pr:` once a fix lands, and `disposition_recommended:` when you have a publish-or-decline opinion. Keep `verdict` describing the defect and `disposition_recommended` describing what to do about it — do not let a decline quietly rewrite a real finding as `not-a-vulnerability`.\n\n## Order of work\n\n1. **Extract the PoC verbatim** into `.triage/advisories/<GHSA-ID>/poc.ts`. Do not clean it up first — a PoC that only works after you fix it is a finding in itself.\n2. **Reproduce against current `main`**: `nub run dev .triage/advisories/<GHSA-ID>/poc.ts`. If it does not reproduce, say so and check whether it ever did (`git log -S`, or test the version the reporter named).\n3. **Decide whether the reproduced behavior is a vulnerability at all.** The recurring question on this repo is whether the attacker controls the **schema** or only the **input**. A schema is application code, so a report needing an attacker-authored schema is usually `not-a-vulnerability`. For prototype reports: is the polluted prototype the returned object's own (contained) or `Object.prototype` itself (real)?\n4. **Enumerate reachability yourself; do not inherit the reporter's.** Reporters test the path they found. Grep every call site that can feed attacker data into the sink and build a route table — precondition per route, and whether each needs only input, or a particular schema shape, or cooperating application code. This is where the severity actually gets decided, and it routinely turns up both routes the reporter missed and routes that are already closed.\n5. **Dedupe the cluster.** Many reports describe one defect. `grep -rl '<keyword>' .triage/advisories/*/results.md`, pick a canonical, cross-link the rest with `duplicate_of`. Fold by underlying defect, never by title keyword.\n6. **Check for public prior art** — an open issue or PR describing the same bug without the security framing. If one exists the defect is already public, which kills the embargo and changes the disposition.\n7. **Land the fix** — see below.\n8. **Then** write `results.md` and draft the reporter note.\n\n## Land the fix first\n\nA close comment's entire value is the line that says where it was fixed. Draft one before a fix exists and you have written a decline, whatever it says — the reporter gets no commit to verify, no release to wait for, and no reason to believe the report was acted on. Colin then has to do the work you were asked to do before he can send anything.\n\nSo the fix is not a follow-up to the triage. It is a step inside it, and it comes before any comment is drafted:\n\n- **Search for existing PRs before writing one.** Popular defects attract near-duplicate community PRs — this repo had seven for a single formatter bug. Read every diff, rank them, and take the best as a base rather than opening an eighth.\n- **Judge the candidates against the whole defect, not the reported symptom.** A patch that fixes the one function in the PoC while three sibling call sites keep the bug is partial. Build a behavior matrix of the cases that matter, run it against `main` and against each candidate patch, and let that table pick the winner. This is also how you catch a patch that trades a crash for silent data loss.\n- **Extend the base rather than starting over**, and preserve the contributor's commits — AGENTS.md covers pushing to a contributor's head ref.\n- **Run what CI runs** before pushing, then confirm CI itself is green.\n- **Do not bump a version and do not cut a release.** Landing on `main` is the goal. Release timing on a security fix is Colin's, and on this repo approving the release workflow's `npm` environment is the one irreversible action.\n\nOnly once the fix is merged do you write the comment, and the comment names the merge commit and the PR.\n\nIf the fix cannot land — the right change is a breaking one, or the affected major is EOL — say so explicitly in `results.md` and draft the comment around what the reporter's actual remedy is. That is a real outcome, not a skipped step.\n\n## Privacy cuts both ways\n\nThe report body may contain an unpatched exploit, so it never goes into a public issue, PR, or commit message. A fix can land publicly while the advisory stays private — describe the defect neutrally in the commit and let the mechanism speak for itself.\n\nThe direction that gets missed: **each reporter can see only their own advisory thread.** A GHSA id in a comment is a dead link to everyone but the maintainers. A duplicate close must never say \"duplicate of GHSA-xxxx\" or \"see the reasoning on that thread\" — the reporter can open neither. Every comment stands alone: state the reasoning inline, and refer to a sibling report as \"a separate report of the same defect\", without the id and without the other reporter's name. Public issues, PRs and commit SHAs are the only cross-references a reporter can follow. The `duplicate_of` link belongs in `results.md`, which is for Colin.\n\n## Publish or decline\n\nA defect can be real and still not be worth a published GHSA. Answer these before recommending either way:\n\n- **Does the attacker control the value, or only the key?** A write the attacker cannot choose the value of — a fixed object, a truthiness flip — needs a second-order sink in the victim's own code before it does anything. That sink is the application's bug, not the library's.\n- **What actually happens at the boundary?** Test the near-miss cases rather than assuming. A gadget that throws on the interesting inputs and only pollutes on the boring ones is much weaker than the report claims.\n- **Is the supported major reachable without cooperating application code?** If the current version needs an unusual schema shape or an app that derives paths from user input, an advisory naming it overstates the exposure.\n- **Is there a patch to point at?** An advisory against an EOL major with no patch turns every install red in `npm audit`, mostly transitively, with \"upgrade the major\" as the only remedy. That is a large ecosystem cost for little defensive gain.\n- **Is it already public?** A defect with an open issue describing it was never embargoable. Fix it in the open.\n\nState the recommendation plainly and name what would reverse it. Where a reachability judgment rests on how common some application pattern is, say that you reasoned it rather than measured it.\n\nCredit is the thing to offer when declining. A reporter who found a real bug that produced a real fix wants attribution — release-notes credit costs nothing and is usually what they were after.\n\n## The reporter note\n\nPer the `triage` skill's `## Drafting the close comment`, plus one advisory-specific pressure: a decline invites you to walk the reporter through the entire severity argument, and the analysis is right there. Do not paste it. Four things belong in the note — it is fixed and where, whether you are publishing, the single reason that decides it, and the offer of credit. Everything else lives in `results.md`.\n\nCorrect a reporter's overstated impact when it is wrong, in one sentence, with what you actually observed. Do not relitigate their CVSS vector line by line.\n",".claude/skills/triage/SKILL.md":"---\nname: triage\ndescription: Investigate a GitHub issue or pull request in colinhacks/zod and write up a durable verdict. Use whenever asked to triage, investigate, review, evaluate, or form an opinion on an issue or PR (by number, URL, or \"the open PR queue\"), and when sweeping many of them in bulk. For a draft SECURITY ADVISORY (a GHSA id, the Security tab, a private vulnerability report) use the `security-advisory` skill instead — it shares this file's conventions but its workflow lands the fix before drafting any comment. Covers where write-ups live on disk (.triage/issues/NNNN/, .triage/prs/NNNN/), the cheap-disqualifier pass that avoids spinning up a worktree for stale PRs, the worktree checkout procedure for PRs, the results.md format that keeps a 250-PR sweep greppable and resumable, how to report back in chat — the final message is all the user sees, so it leads with a code block showing the problem and a short outline of the fix — and the maintainer-voice comment it drafts whenever the verdict is to close.\n---\n\n# Investigating issues and PRs\n\nProduce a write-up a maintainer can act on without re-reading the thread. The durable deliverable is always a file on disk — sweeps span many sessions and agents, so anything living only in a transcript is lost. But the file is not what the user reads first: your final chat message is, and for most tickets it is the only thing they will ever read. Both matter, and `## Reporting back` below is not optional.\n\n**Read-only on GitHub.** Never comment, label, assign, close, review, approve, or merge unless the user explicitly asks in this session. You are forming an opinion, not publishing one. Drafting the close comment described below is part of forming it — writing the text is required, sending it is not yours to do.\n\n## Where things live\n\nEverything goes under `.triage/` in the **root repo**, which is gitignored — write freely, it never shows up in `git status`.\n\n```\n.triage/\n  index.md                    # the original issue sweep index — hand-maintained prose, still the canonical issue queue\n  issues/index.md             # generated from frontmatter by reindex.mjs\n  issues/<N>/results.md       # the write-up; scratch files live beside it\n  prs/index.md                # generated\n  prs/<N>/results.md\n  advisories/<GHSA-ID>/results.md   # draft security advisories from the Security tab\n  signal-report.md            # generated by scripts/triage-signal.ts — ranks the open backlog\n```\n\nOne directory per ticket, named by bare GitHub number (no leading zeros, no `issue-`/`pr-` prefix) — or by bare GHSA id for an advisory. `results.md` is the write-up. Everything else you need — repro scripts, a saved `gh pr diff`, test fixtures, benchmark output, notes — goes in that same directory. Name scratch files descriptively (`repro.ts`, `diff.patch`, `bench-before.txt`); there is no naming rule beyond not colliding with `results.md`.\n\nFor an issue repro, `nub run dev .triage/issues/<N>/repro.ts` from the root repo works directly.\n\nA repro must sit **inside** the checkout it is testing, because module resolution walks up from the file, not from the cwd — a script under `.triage/` cannot resolve `zod` from a PR worktree. Keep the canonical copy in the ticket directory and copy it in to run: `cp .triage/prs/<N>/repro.ts <worktree>/repro-<N>.ts`. That is also what lets you run the identical file against both `main` and the PR and diff the output, which is the cheapest way to characterize a behavior change. For a probe needing GC or other flags, invoke Nub with the flags directly: `nub --expose-gc --conditions=@zod/source repro-<N>.ts`.\n\n### Writing to the root repo from a PR worktree\n\nThis is the one mistake that silently destroys work. A worktree has its **own** gitignored `.triage/`, so a relative write from inside it lands somewhere nobody will ever look.\n\nResolve the root once and always write through it:\n\n```bash\nZOD_ROOT=$(git worktree list | head -1 | awk '{print $1}')   # correct from inside any worktree\nmkdir -p \"$ZOD_ROOT/.triage/prs/<N>\"\n```\n\nEvery path you write must start with `$ZOD_ROOT`. Investigate in the worktree, record in the root.\n\n## results.md format\n\nOpen with YAML frontmatter. It is what makes a 250-PR sweep aggregatable and resumable — an interrupted sweep is resumed by grepping `status`, never by re-reading write-ups.\n\n```yaml\n---\nnumber: 5928\ntype: pr                    # pr | issue\nstatus: done                # in-progress | done\nverdict: merge-with-changes\ntitle: \"fix: preserve brand through .optional()\"\nauthor: someuser\nurl: https://github.com/colinhacks/zod/pull/5928\ninvestigated: 2026-08-08\n---\n```\n\n`verdict` for **PRs** — one of:\n\n- `merge-as-is` — correct, tested, in scope. Say so plainly.\n- `merge-with-changes` — right idea, needs specific edits. Enumerate them concretely enough to apply.\n- `needs-author-changes` — blocked on something only the author can supply (repro, rebase, scope cut).\n- `decline` — out of scope or wrong approach. Give the one or two strongest reasons, plus the escape hatch users actually have.\n- `superseded` — already fixed on `main` or landed via another PR. Name the commit or PR.\n- `needs-maintainer-decision` — the code is fine; the call is a product/API judgment that is Colin's to make. State the tradeoff and your recommendation.\n\n`verdict` for **issues** — reuse the vocabulary already established in `.triage/index.md`: `good-idea`, `already-fixed`, `needs-maintainer-input`, `not-recommended`, `needs-more-reproduction`, `duplicate` (add a `duplicate_of: NNNN` key alongside it).\n\n`verdict` for **advisories** — one of:\n\n- `valid` — a real vulnerability in zod, reproducible on current `main`. State the affected versions and the fix.\n- `not-a-vulnerability` — the behavior is intended, requires the app to already be doing something unsafe, or is a plain bug with no security consequence. This is the most common outcome; say which.\n- `already-fixed` — real, but no longer reproduces on `main`. Name the commit or release that fixed it.\n- `duplicate` — the same defect as another advisory (add `duplicate_of: GHSA-xxxx-xxxx-xxxx`). The `__proto__` reports cluster heavily; check before concluding.\n- `needs-reporter-info` — the report is too vague to reproduce, or the PoC does not run.\n- `needs-maintainer-decision` — real behavior, but whether it counts as a vulnerability is a threat-model call that is Colin's to make.\n\nAdvisory frontmatter swaps `number:` for `ghsa:` and adds `severity_claimed:` (what the reporter asserted) alongside `severity_actual:` (your assessment, or `none`).\n\nSet `status: in-progress` in a stub the moment you start, so a parallel agent or a later session does not duplicate the work. Flip to `done` before you report back.\n\n**One pass. Finish the ticket.** A maintainer question is never a reason to leave a write-up unfinished — investigate fully, commit to a recommended `verdict`, write every section including `## Proposed comment`, and flip `status: done`. Anything genuinely needing the maintainer goes in `## Open questions` (below) *inside the finished file*, so their answer is a one-line amendment to a complete write-up rather than a second investigation. Never hand back a provisional verdict, a `status: in-progress` file, or a promise to finalize later; never wait on an answer before writing. `verdict: TBD` is not a verdict. The only thing that legitimately blocks `done` is evidence you could not obtain — a PR that will not build, a repro that needs credentials — and that is `needs-author-changes` or `needs-more-reproduction`, which are verdicts, not deferrals.\n\nAfter the frontmatter, write prose under these headings. Match the density of the existing issue write-ups in `.triage/issues/*/results.md` — they are the house style; skim one before your first write-up.\n\n- `## Request` / `## Change` — what is actually being asked or changed, in your own words. Include the thread's evolution when maintainers or the author already moved the discussion.\n- `## Current state` — what the code on `main` does **today**, with `file.ts:line` references. Most old tickets were filed against v3 or early v4 and are stale on arrival; this section is usually where the verdict comes from.\n- `## Analysis` — correctness, edge cases, type-level implications, perf, and whether the tests actually cover the claim.\n- `## Recommendation` — the action to take, and the substance of the reply to post. Do not draft it in maintainer voice unless asked, with one standing exception below.\n- `## Proposed comment` — **required whenever the recommendation is to close the ticket**, in a fenced block, ready to paste. See below.\n- `## Open questions` — **only** for calls that are genuinely the maintainer's: product/API direction, a breaking-change tradeoff, taste, or picking between two contributors' PRs. Omit the heading entirely when there are none, which is the common case. Never park a question here that the code, the tests, `git log`, or an existing write-up answers — that is research you owe, not a decision they owe. See below.\n\n### Writing `## Open questions`\n\nEach question is answerable cold, by someone who has not read the write-up and will not open it. One line for what is at stake, then 2–4 lettered options, each with a one-line consequence, with your recommendation marked and placed first. Write in behavior, not identifiers: \"should a schema that declares a field named `__proto__` still reject it?\" not \"should `handleCatchall` precede the `keySet` check?\".\n\nThe verdict above already assumes your recommended option. Say which verdict each *other* option would produce, so the maintainer can see the cost of disagreeing without asking you to redo the analysis:\n\n```markdown\n## Open questions\n\n**Q1 — Should `.strict()` reject an own `__proto__` key, or keep silently dropping it?** Runtime breaking change for anyone posting JSON bodies through `.strict()`.\n\n- **(A) Report it — take the PR.** ← recommended. Restores \"strict rejects every unknown key\". Verdict as written: `merge-with-changes`.\n- (B) Keep dropping it, document the exemption. Zero breakage; the one key an attacker actually sends stays the one key strict does not flag. → `decline`.\n- (C) Report it behind a config flag. No breakage; a permanent knob for a two-line behavior. → `needs-author-changes`.\n```\n\nWhen the answer arrives, amend the file in place: set the final `verdict`, fold the decision into `## Recommendation`, rewrite `## Proposed comment` to match, and leave the question and its answer recorded so nobody relitigates it. `status` stays `done` throughout — it was never not done.\n\n### Drafting the close comment\n\nAny verdict whose action is \"close this\" gets a drafted reply: `decline` and `superseded` on a PR, `not-recommended`, `already-fixed`, and `duplicate` on an issue, and `not-a-vulnerability`, `already-fixed`, and `duplicate` on an advisory. Closing is the one outcome where a human is going to have to say something to a real person who spent time on this, so writing that sentence is part of the triage, not a follow-up task.\n\nFollow the \"Commenting on issues and PRs\" section of AGENTS.md for the maintainer voice. Succinct, conversational, friendly. Lead with the decision, give the one or two strongest reasons, name the escape hatch the user actually has, and close warmly. Two or three sentences is usually the whole thing.\n\nFor an issue reporting that `z.coerce.number()` accepts `null`, the whole draft is:\n\n```markdown\nWorking as intended. `z.coerce.number()` runs `Number()` on the input before it validates, and `Number(null)` is `0` — the coercion happens first, so there is nothing left to reject by the time the number check runs.\n\nIf you want these rejected, use plain `z.number()` and do the coercion at the boundary where you actually know the input is a numeric string. Closing this out — thanks for the writeup 👍\n```\n\nNote what it does not do: no bullet list of every objection, no apology, no \"great question\", no asking whether the reporter agrees. Decision, reason, escape hatch, closer.\n\n#### The investigation's depth never sets the comment's length\n\nThis is the rule that gets broken, and it gets broken hardest on exactly the tickets where the investigation went deepest. A week of benchmarks, a prior-art trail, a prototype, an ecosystem comparison — all of it feels load-bearing, so it leaks into the reply and the two-or-three-sentence bar quietly becomes five paragraphs. **The evidence you are proudest of is the first thing to cut.** It belongs in `results.md`, which is where a maintainer looks when someone pushes back; the comment is not the place to prove you did the work.\n\nA worked case. A draft advisory reported ReDoS in `z.fromJSONSchema()` — real, reproducible, two sinks, exponential. The investigation ran long: measured timings, prior art in a closed issue and a closed fix PR, a 54MB dependency weighed, a zero-dep detector prototyped and evaluated, Ajv's published security contract pulled in as precedent. The drafted comment carried most of that. What the maintainer actually posted, in full:\n\n```markdown\nClosing this out — declining. `z.fromJSONSchema` is designed to convert arbitrary JSON Schema to a Zod schema, and it up to the user to do their own out of band validation. Thanks for the report.\n```\n\nThree sentences, and **this is what most declining comments should look like.** Read what it drops and adopt each as a rule:\n\n- **No cross-references.** No issue number, no PR number, no prior-art trail — even when a maintainer closed the identical report before. That history justifies the decision to *you*; it does not need to be shown to the reporter.\n- **No numbers, no engineering.** No timings, no dependency sizes, no analysis of what a guard would cost or why the cheap version fails. None of it changes the answer.\n- **No external precedent.** How another library documents the same contract is research that settles your verdict, not material for the reply.\n- **The reason is design intent, asserted — not an argument, defended.** \"Is designed to convert arbitrary JSON Schema\" states the contract in one clause and stops. A maintainer declares scope; they do not litigate it. Once you find yourself building a case, you have written the wrong document.\n- **No concession and no promise.** Do not offer \"you have a fair point about X\", and do not commit to a docs change, a new option, or any follow-up. Real follow-ups get filed as their own issue; a decline that promises work is no longer a decline.\n\nKeep only: the decision, one clause of reason, what the user does instead (\"their own out of band validation\"), and a short closer. When a drafted comment runs past three or four sentences, that is the signal to cut, not to reorganize.\n\nDrafting is not posting. The draft sits in `results.md` and in your chat message; you never send it unless the user explicitly asks in this session. For an advisory the draft is the note to the reporter, and it must not restate an unpatched exploit.\n\nThe length rule binds hardest on an advisory decline, which is where it is most often broken: a reporter who filed a real, reproducible defect invites you to walk them through the whole severity argument, and the analysis you just spent hours on is right there. Do not paste it. The reporter needs four things — it is fixed and where, you are not publishing, the one reason that actually decides it, and the offer of credit. Everything else belongs in `results.md`. A decline that runs to six paragraphs is a worse decline, not a more rigorous one.\n\n## Reporting back\n\n`results.md` is the archive. **Your final chat message is the interface, and for most tickets it is the only thing the user will ever read.** Assume they see it hours later, in a queue, without the thread in front of them, without opening the file, and without any of your working context. \"Investigated #5928, write-up is in `.triage/prs/5928/results.md`\" has delivered nothing — it just moved the work onto them.\n\nSo the last message before you rest always carries these three things, in this order, plus a fourth when the verdict is to close:\n\n**1. The problem, as a code block.** Not a paragraph describing the code block — the actual minimal snippet, with the wrong result written next to it as a comment. This is the single highest-value thing in the message: it is what lets the user confirm or reject your read in five seconds instead of re-deriving the whole ticket. Strip the reporter's app scaffolding down to the schema and the one call that misbehaves, and show actual versus expected.\n\n```ts\nconst schema = z.coerce.number();\n\nschema.parse(null); // → 0\nschema.parse(\"\"); // → 0\nschema.parse([]); // → 0\n// reporter expected each of these to fail validation\n```\n\nType-level tickets get the same treatment, with the inferred type as a comment instead of the parse result:\n\n```ts\ntype Out = z.infer<typeof schema>;\n// actual:   string | undefined\n// expected: string\n```\n\n**2. The shape of the fix, in two or three sentences.** Where it goes (`file.ts:line`), what changes, and how big it really is — one-line type tweak, new method on `ZodType`, or a refactor that touches every wrapper. If the fix is not worth doing, say what the user does instead. Do not paste the patch; the user is deciding whether to spend time here, not applying it from chat.\n\n**3. The verdict and the path.** The frontmatter `verdict` verbatim, plus a link to the ticket and the `results.md` path for the detail you left out.\n\n**4. The proposed comment, if the verdict is to close.** Repeat the `## Proposed comment` draft in the message, in its own fenced block, so the user can read and paste it without opening the file. Say plainly that you have not posted it.\n\n**5. The open questions, if the write-up has any.** Repeat them from `## Open questions`, options and all, so they are answerable without opening the file. Frame them as what they are — the ticket is triaged and the verdict is recorded; the answer only confirms or redirects it. Never present them as a request for permission to finish, never say the write-up is waiting on them, and never end on \"send me your answer and I'll finalize\". The work is already done.\n\nKeep the whole thing short — the code block plus a few sentences. The message is a summary that stands alone, not a condensed replay of the write-up, and it is not the place for the investigation narrative, the commands you ran, or the branches you ruled out. Those are what the file is for.\n\nTwo adjustments by shape of the task:\n\n- **Nothing reproduced** — an `already-fixed` or `not-a-vulnerability` verdict still gets a code block. Show the reporter's own snippet and the *correct* output it now produces on `main`; that is the evidence, and without it the user just has your word.\n- **A bulk sweep** — one summary table of ticket, verdict, and a one-line reason, and code blocks only for the two or three tickets that genuinely need a decision. Twenty code blocks is the same as none. Every close still gets its drafted comment written into its own `results.md`; the message just says which tickets have one waiting instead of inlining them all.\n- **More than one ticket has a comment to post** — pair each one: the link on its own line, its fenced comment block **directly underneath it**, then the next link. Never collect the links in one place and the comment blocks in another, and never make the user match a block to a link by reading the prose between them. This is how a set of draft advisories is always handed back, since the user posts them one at a time with the link and its text side by side.\n\n  ```markdown\n  ### [GHSA-xxxx-xxxx-xxxx](https://github.com/colinhacks/zod/security/advisories/GHSA-xxxx-xxxx-xxxx) — valid\n\n  <the comment for that advisory, in its own fenced block>\n\n  ### [GHSA-yyyy-yyyy-yyyy](https://github.com/colinhacks/zod/security/advisories/GHSA-yyyy-yyyy-yyyy) — duplicate\n\n  <the comment for that advisory, in its own fenced block>\n  ```\n\n## Investigating an issue\n\nNo worktree — read the code on `main`.\n\n1. `gh issue view <N> -R colinhacks/zod --comments` and read the whole thread, including maintainer replies. A prior decision from Colin usually settles it.\n2. Check for duplicates and prior art: `gh search issues --repo colinhacks/zod '<keywords>'`, and grep `.triage/issues/*/results.md` — the sweep may already cover it. Cross-link with a relative `../NNNN/results.md`.\n3. Reproduce against current source before believing the report. Write the repro to `.triage/issues/<N>/repro.ts` and run `nub run dev` on it. A large share of open issues are already fixed.\n4. Brainstorm before concluding. Consider the fix, its blast radius, whether it belongs in core at all, and what userland escape hatch already exists.\n5. Write `results.md`, then report back per `## Reporting back`.\n\n## Investigating a draft advisory\n\n**Use the `security-advisory` skill instead — invoke it before you start.** Draft advisories live in the Security tab, so nothing in `gh issue list` surfaces them, and the workflow diverges from this one in ways that matter: the report is private in both directions, the burden of proof sits with the reporter, the fix must be **landed before any comment is drafted**, and \"real defect\" and \"publishable vulnerability\" are separate calls.\n\nWhat carries over from this skill: where files live, the `results.md` frontmatter and headings, the close-comment voice, and `## Reporting back`. Advisory write-ups go to `.triage/advisories/<GHSA-ID>/results.md` with `ghsa:` in place of `number:`.\n\nNever accept, publish, decline, or comment on an advisory. The write-up and the drafted note are the deliverables; the disposition is Colin's.\n\n## Investigating a PR\n\nCheap pass first — most of a 250-PR backlog is resolved without ever checking out code.\n\n```bash\ngh pr view <N> -R colinhacks/zod --json number,title,author,state,isDraft,mergeable,mergeStateStatus,baseRefName,headRefName,headRepositoryOwner,maintainerCanModify,createdAt,updatedAt,additions,deletions,changedFiles,labels,comments\ngh pr diff <N> -R colinhacks/zod\n```\n\nDisqualify early and write the write-up straight from this, no worktree, when the diff is small and self-evident, or when the PR targets v3 / duplicates a landed change / is already superseded by `main` / is abandoned with unresolved conflicts. Verify `superseded` against actual code on `main` — do not infer it from dates.\n\nOtherwise check it out (this repo's convention, per AGENTS.md):\n\n```bash\ngit fetch origin pull/<N>/head:pr-<N>\ngit worktree add ~/.cursor/worktrees/zod/pr-<N> pr-<N>\ncd ~/.cursor/worktrees/zod/pr-<N>\nnub install --frozen-lockfile      # fast, Nub's store is shared\n```\n\nDo **not** use `gh pr checkout --detach` — it detaches your current working tree instead of creating a worktree.\n\nThen, in the worktree:\n\n1. Read the diff in full context, not just the hunks. What did it touch that the author did not think about?\n2. `git log --oneline main..HEAD` — is this rebased on anything current, or written against a version of the code that has since moved?\n3. Run the tests the change implicates: `nub exec --node vitest run <path>`. Run the full suite only when the change is broad.\n4. Judge test coverage against the repo's own bar: a feature or bug fix without a test is incomplete, and tests must be TypeScript.\n5. Probe the edge cases the author's tests skip — write them into `$ZOD_ROOT/.triage/prs/<N>/` and run them. A passing suite over a shallow test is not evidence.\n6. Write `$ZOD_ROOT/.triage/prs/<N>/results.md`, then report back per `## Reporting back`.\n\n**Clean up when done**, or worktrees accumulate across a large sweep:\n\n```bash\ncd \"$ZOD_ROOT\"\ngit worktree remove ~/.cursor/worktrees/zod/pr-<N> && git branch -D pr-<N>\n```\n\nKeep the worktree only when the verdict is `merge-with-changes` and you have been asked to push the changes; AGENTS.md covers pushing back to a contributor's fork, including preserving their commits.\n\n## Sweeping in bulk\n\nSeed the queue, then work it:\n\n```bash\ngh pr list -R colinhacks/zod --state open --limit 400 --json number,title,updatedAt\n```\n\nResume by asking disk, not memory:\n\n```bash\ngrep -l 'status: done' .triage/prs/*/results.md | wc -l                    # progress\ngrep -h '^verdict:' .triage/prs/*/results.md | sort | uniq -c              # verdict spread\nnub --node .claude/skills/triage/scripts/reindex.mjs prs             # regenerate .triage/prs/index.md\n```\n\n`reindex.mjs` rebuilds an index table from frontmatter, so the index is derived and never hand-edited for PRs. Pass `issues`, `prs`, or `advisories`.\n\n`nub run dev scripts/triage-signal.ts` regenerates `.triage/signal-report.md`, which ranks the whole open backlog by engagement, flags security content, and groups likely-duplicate clusters. Start a sweep there rather than at the top of `gh pr list` — roughly a third of the open backlog is duplicates, and the clusters collapse many tickets into one decision.\n\nWhen parallelizing across sub-agents, give each agent a disjoint set of numbers and have each write only its own ticket directories. Never have two agents share a results file.\n","AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Development Commands\n\nUse Nub exclusively for dependency management, scripts, and TypeScript execution. The root `package.json` declares the workspaces and pins Nub; `.nvmrc` selects Node. Nub reads and writes the existing `pnpm-lock.yaml` without requiring pnpm.\n\nRun scripts with `nub run`, repository TypeScript with `nub <file>`, and installed tools with `nub exec --node <tool>`. Keep third-party tools and built-package compatibility probes on plain Node; Nub's augmentation must not mask resolution errors or change test behavior. Use `nub install --frozen-lockfile` for reproducible installs and `nub add` / `nub remove` to manage dependencies.\n\nKey commands:\n\n- `nub run build` - Build all packages (runs recursive build command)\n- `nub exec --node vitest run` - Run all tests with Vitest. Includes the compile-mode project, which re-runs the zod tests with global AOT compilation enabled (see `wiki/compile.md`).\n- `nub exec --node vitest run <path>` - Run specific test file (e.g., `packages/zod/src/v4/classic/tests/string.test.ts`)\n- `nub exec --node vitest run <path> -t \"<pattern>\"` - Run specific test(s) within a file (e.g., `-t \"MAC\"`)\n- `nub exec --node vitest run --update` - Update all test snapshots\n- `nub exec --node vitest run <path> --update` - Update snapshots for specific test file\n- `nub run test:watch` - Run tests in watch mode\n- `nub exec --node vitest run --coverage` - Run tests with coverage report\n- `nub run test:compile` - Focused alias for just the compile-mode project. Already covered by `nub run test`; use this when iterating on compile-related changes.\n- `nub run dev` - Execute TypeScript with Nub under source conditions\n- `nub run dev <file>` - Execute `<file>` with Nub & proper resolution conditions. Usually use for `play.ts`.\n- `nub run dev:play` - Quick alias to run play.ts for experimentation\n- `nub run check:comments` - Fail on stacked `//` comment lines (`--fix` joins them)\n- `nub run lint` - Run biome linter with auto-fix\n- `nub run format` - Format code with biome\n- `nub run fix` - Run both format and lint\n\n## Rules\n\n- Nub v0.8.3 and Node.js v24+ required; Nub provisions Node from `.nvmrc`\n- ES modules are used throughout (`\"type\": \"module\"`)\n- All tests must be written in TypeScript - never use JavaScript\n- Use `play.ts` for quick experimentation; use proper tests for all permanent test cases\n- Features without tests are incomplete - every new feature or bug fix needs test coverage\n- Don't skip tests due to type issues - fix the types instead\n- Test both success and failure cases with edge cases\n- Keep added tests as minimal and dense as possible without sacrificing comprehensiveness; avoid redundant assertions or broad fixtures when a focused case proves the behavior.\n- No log statements (`console.log`, `debugger`) in tests or production code\n- Never stack prose across consecutive `//` lines. Lines have no maximum width here — the editor wraps for display — so a paragraph split across several `//` lines is just a hard-wrapped line, and hard wrapping breaks search, diffs and editing. Write one long `//` instead. `nub run check:comments` enforces this in pre-commit and CI; `--fix` joins the offenders. Commented-out code, `@ts-`/`@__NO_SIDE_EFFECTS__`-style pragmas, bullet lists, and blocks separated by a bare `//` are exempt. When two adjacent comments describe two different statements, separate them with a blank line rather than joining them.\n- Keep comments SHORT AND TIGHT — one lowercase sentence fragment, one clause, no trailing period. Never a capitalized full sentence, and never two of them. Extreme concision: say only what the code cannot say, and cut the setup sentence, the recap, and the same point restated in different words; a comment that needs three sentences usually means the code should be clearer. Identifiers keep their real casing (`Error`, `parse()`), only the prose is lowercase.\n- Ask before generating new files\n- Use `util.defineLazy()` for computed properties to avoid circular dependencies\n- Never branch on specific schema types in shared code. No `def.type === \"optional\"` conditionals, no hardcoded lists of wrapper type names, no walks up the wrapper chain hunting for a particular type. Every schema type added later silently falls through such a check, and the list is wrong the moment someone writes a new wrapper. When a shared path needs to know something about a schema, express it as a structural property on the internals — `optin`/`optout`, `values`, `pattern`, `propValues` — and let each type declare its own answer. This is not negotiable in the parse paths; a PR that adds edge-case conditional logic keyed on schema types will be rejected regardless of how well it is tested.\n- Performance is critical - parameter reassignment is allowed for optimization\n- Any change to `packages/zod/src` must be weighed on **all three axes: runtime performance, memory consumption, and bundle size** — see \"The three axes\" below. A change that improves one and is only checked on that one is not finished.\n- ALWAYS use the `gh` CLI to fetch GitHub information (issues, PRs, etc.) instead of relying on web search or assumptions\n- Keep JSDoc as minimal as possible. A self-explanatory type or symbol name needs no doc comment. When a comment is genuinely required, write one short sentence describing behavior — not history, rationale, or examples. Don't add interface-level JSDoc that just restates the interface name.\n- When you've modified a PR (or opened/closed/commented on one), include the PR URL liberally in summary messages — at minimum once at the end of any reply that touched it\n- When creating a PR, do not include a separate test plan section in the body. Link to any relevant issues under discussion, and use the same copywriting guidelines from \"Commenting on issues and PRs\": concise maintainer voice, prose over templates, and validation details only when they are material to the reader.\n- Format validators (`z.iso.*`, `z.email()`, `z.url()`, `z.uuid()`, …) are deliberately narrower than the specs they're named after. \"The spec allows X\" is not a reason to accept X — see \"Format validators: spec compliance is not the bar\" below.\n- NEVER bump the version in `packages/zod/package.json` (or any package's `package.json`), and NEVER run `gh workflow run release.yml`. Publishing is a maintainer-only dispatch behind an approval. If a version bump is genuinely needed, ask first.\n\n## The three axes\n\nZod is judged on **runtime performance**, **memory consumption**, and **bundle size** at once, and they trade against each other constantly. Optimizing one in isolation is how regressions land: a change that buys bundle bytes can cost construction speed, and one that saves memory can cost both. Any non-trivial change to `packages/zod/src` — and any change to `core/` at all, since every build ships it — needs a number on all three before it is done. Report them together, including the ones that got worse.\n\n`zod/mini` deserves its own line on the bundle axis: it is sold on size, so a fixed cost lands very differently there (~200 B is 6% of the smallest mini bundle and 1% of classic's).\n\n| axis    | how to measure                                                                                                                                    |\n| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| runtime | `packages/bench/*.ts` via `nub run bench <name>`, plus construction cost — parse and construction move independently                                 |\n| memory  | `packages/bench/memory/schema-footprint.ts` and `realworld.ts` (retained bytes per schema; run under `--expose-gc`)                               |\n| bundle  | bundle a `packages/treeshake` fixture with esbuild `--minify` under `--conditions=@zod/source` and gzip it, for both a classic and a mini fixture |\n\n### Benchmarking traps\n\nThese produced hours of wrong conclusions; check them before believing a number.\n\n- **Check `uptime` first.** A loaded machine invents effects of ±16%. Numbers taken above a load average of ~8 are worthless.\n- **Interleave the two revisions.** Running all of A then all of B puts any drift entirely on whichever ran second. Alternating round-by-round took per-side spread from 2.5x to 4%.\n- **Loading two zod revisions into one process makes call sites polymorphic.** It is fine for comparing construction, unreliable for tight leaf parses.\n- **Allocating benchmarks can't be timed by a fixed-duration loop.** `z.array().parse()` allocates per call, so a time-boxed harness samples whatever the collector is doing — it gave +8.2% and −17.3% on consecutive identical runs. Use a fixed iteration count with `gc()` between samples and take the minimum.\n- **Make sure the work isn't optimized away.** If a micro-benchmark reports ~1e9 ops/sec, V8 eliminated the loop; consume the result.\n\n### Property placement, specifically\n\nMost of the memory in a schema is its own-property count, not its closures. V8 sizes an instance's backing store in steps and `$constructor` assigns nothing itself, so instances get no in-object slots: **≤12 own properties cost 128 bytes, 13–20 cost 848, ≥21 cost 1616** (`packages/bench/memory/prop-slack.ts` measures this). Members therefore live on the prototype and materialize per instance on first read.\n\nIf you touch that machinery, three things bite:\n\n- **Moving a property between definition sites changes its descriptor**, and writability/enumerability/configurability are part of the public contract. `packages/bench/memory/` has a surface-diff approach for this: dump every descriptor, alias and assign/delete semantic on both revisions and diff them. It caught four real regressions that tests did not.\n- **Redefining an accessor demotes the object to dictionary mode**, which cost 2x on `z.object().parse` once. Run `packages/bench/memory/dict-mode.ts` after any change here — every instance must report `fast`.\n- **A bound function pays a call-time trampoline.** Fine for cold builder methods, measurably not fine on the parse path.\n\n## Format validators: spec compliance is not the bar\n\nZod's format validators — `z.iso.*`, `z.email()`, `z.url()`, `z.uuid()`, and the rest — are named after specs but do not implement them. Each one matches the profile that real producers emit and real consumers accept, which is almost always far narrower than what the grammar permits. That narrowness is the product, not a gap in it.\n\nSo **\"the spec allows X, therefore Zod must accept X\" is not an argument**, and a PR that widens a format regex on that basis alone will be closed. The question is never what the spec permits; it's whether accepting the wider form helps more people than rejecting it hurts. A form that shows up in real payloads and breaks real integrations is worth fixing. A form that only appears when someone reads the grammar and constructs a string from it is not — when one of those reaches a validator in production it's a bug or an attack, and rejecting it is the useful behavior.\n\nThree things make a widening request an automatic no:\n\n- **The spec permits the form only \"by mutual agreement\" of the two parties.** A general-purpose validator is never party to that agreement, so it can't honor the clause. ISO 8601 expanded years (`+010000-01-01T00:00:00.000Z`) are the canonical case: the standard also requires the parties to agree the digit count, and runtimes disagree — JavaScript emits six digits, Java's `Instant` emits five, Python rejects both. Declined in #6154.\n- **The wider form breaks the JSON Schema output.** `z.iso.datetime()` and `z.iso.date()` emit `format: \"date-time\"` and `format: \"date\"`, which mean RFC 3339 — four-digit years, no sign. Widening those regexes makes `z.toJSONSchema()` emit a pattern that conformant consumers reject.\n- **The cost lands on everyone.** These regexes live in `core/regexes.ts`, so every added alternation is bytes in every bundle including `zod/mini`, plus more backtracking on a hot parse path. Weigh it on all three axes above before you argue for it at all.\n\nFor calibration on how narrow these already are: `z.iso.datetime()` rejects basic format (`20200101T061500Z`), week dates (`2020-W01-1`), ordinal dates (`2020-001`), comma decimal separators, and `24:00`. Every one of those is valid ISO 8601. None of them are going in either.\n\nPoint anyone who genuinely needs a wider format at `z.string().regex()` or `z.string().refine()`. That escape hatch is the answer, not a wider default.\n\n## Triaging issues and PRs\n\nFollow the `triage` skill at [`.claude/skills/triage/SKILL.md`](.claude/skills/triage/SKILL.md) whenever you're asked to investigate, triage, or form an opinion on an issue or PR. It is the single source of truth for the procedure. Claude and Codex both auto-discover it (Codex via the `.codex/skills` symlink); read it directly if your agent doesn't.\n\nFor a draft **security advisory** — a GHSA id, the Security tab, a private vulnerability report — use the `security-advisory` skill at [`.claude/skills/security-advisory/SKILL.md`](.claude/skills/security-advisory/SKILL.md) instead. It shares the conventions above but reorders the work: the fix lands on `main` before any reporter comment is drafted, since the comment's whole value is the PR it links to.\n\nWrite-ups live in the gitignored `.triage/` tree — one directory per ticket, `.triage/issues/NNNN/results.md` and `.triage/prs/NNNN/results.md`, with scratch files and repros alongside. When investigating a PR from its worktree, resolve the root repo first (`git worktree list | head -1`) and write results back there; a relative write lands in the worktree's own ignored `.triage/` where nobody will find it.\n\n## Iterating on a contributor PR in a worktree\n\nWhen asked to make changes on top of an open PR (e.g. as a maintainer review suggestion), use a worktree so `main` stays clean:\n\n```bash\n# 1. Fetch the PR as a local branch and create a worktree for it\ngit fetch origin pull/<N>/head:pr-<N>\ngit worktree add ~/.cursor/worktrees/zod/pr-<N> pr-<N>\ncd ~/.cursor/worktrees/zod/pr-<N>\nnub install --frozen-lockfile   # fast, Nub's store is shared across worktrees\n\n# 2. Look up the PR's head info — you'll need the contributor's fork URL\n#    and the head ref name to push back.\ngh pr view <N> --repo colinhacks/zod \\\n  --json headRefName,headRepositoryOwner,maintainerCanModify\n\n# 3. If maintainerCanModify is true, add the fork as a remote and push to\n#    the PR's head ref (NOT to your local branch name).\ngit remote add <contributor> git@github.com:<contributor>/zod.git\ngit push <contributor> pr-<N>:<headRefName>                 # first push\ngit push <contributor> pr-<N>:<headRefName> --force-with-lease   # for amends\n```\n\nNotes:\n\n- Do NOT use `gh pr checkout --detach` for this — it moves your _current_ working tree into detached HEAD instead of creating a worktree.\n- Husky pre-commit runs biome format/lint via lint-staged; pre-push runs the full vitest suite. Both are fast and act as a safety net — don't bypass with `--no-verify` unless you have a specific reason.\n- **Preserve contributor commits.** Never `git reset --hard` or otherwise rewrite history that erases the contributor's work, even if you're rewriting the actual change. They need to stay in the PR's commit list to get credit on the merged PR. If their approach was wrong, add a `Revert \"...\"` commit (or just a plain commit that undoes those lines) and then add your replacement commit on top. Force-pushing a single clobbering commit strips them from the GitHub contributors graph.\n- When done, clean up: `git worktree remove ~/.cursor/worktrees/zod/pr-<N>` and `git branch -D pr-<N>` (and optionally `git remote remove <contributor>`).\n\n## Commenting on issues and PRs\n\nWhen posting on a maintainer's behalf via `gh` (PR comments, issue comments, reviews), match the house tone. Load the `prose-writing` skill before you write any of it — it is the general copy guide, and this section layers on top of it. The register is authoritative and friendly — concise, not bubbly, not over-explaining, not effusive. Comments come from a maintainer handing down decisions, not negotiating them. Friendly does not mean deferential.\n\n- Exclamation points are fine in moderation, especially to soften a decline or close out a thread (\"Thanks for looking into this!\"). Don't stack them and don't sprinkle them through technical writeups.\n- Skip effusive praise: \"Great work\", \"Awesome\", \"Thanks so much for this\", \"Thanks for the careful writeup\", \"you clearly read the RFC\". A short flat-affect affirmation walks the line well — \"Good investigation.\" or \"Solid catch.\" with a period, no exclamation, no superlatives. Warmth otherwise comes from a short closer (\"Thanks for looking into this, though 👍\"), not a preamble that butters up the contributor before the decision.\n- No \"PTAL\", \"WDYT\", or sign-off flourishes asking the contributor to re-review changes the maintainer pushed on top. State what changed and the merge intent. (\"LGTM\" is fine as a literal verdict at the end of a substantive review, not as a sign-off.)\n- When the user gives you exact wording for a comment, use it verbatim (fixing only obvious typos). Do not \"improve\" their phrasing to match this style guide — their direct instruction wins.\n- Lead with the decision or action: \"Going to merge as-is.\" \"Closing this out.\" \"I'd be open to a top-level utility but not as a method.\" Then the reasoning.\n- First person, owned opinions. \"I don't think this should be a method.\" \"I'm hesitant to add this.\" Don't hide behind passive voice or \"we could perhaps consider\".\n- Speak with authority. No hedging (\"maybe\", \"I think perhaps\", \"if that's okay\"), no apologizing for decisions, no asking permission to land changes the maintainer has already made. Decisions are stated as decisions.\n- Be direct when declining, but not curt. \"out of scope\", \"behaving as intended\", \"this is more complicated than it looks\" — firm, with a concrete reason. A friendly closer (\"thanks for looking into this\") is fine.\n- Cross-reference by number: `#4433`, `commit 2f8414bc`, `merged in #5718`. Concrete and verifiable.\n- Length matches substance, and substance is almost never as much as you think. Two or three sentences closes most threads. How long you spent investigating has nothing to do with how long the reply should be — a week of benchmarking still closes in three sentences. The `triage` skill works this through against a real decline.\n- No file paths, line numbers, or symbol names unless the reader has to open that file. They turn a two-line decision into a report, and nobody asked you to show your work. Say what the code does instead — \"the parse context is per-call state now\", not \"`core/memoizer.ts:133` writes onto the context\". The detail belongs in the `.triage/` write-up.\n- Pick the one or two strongest reasons and write them as prose. Resist enumerating every objection in a bullet list — even when each point is fair, it reads as piling on. The strongest argument plus a concrete escape hatch (e.g. \"`z.email().max(254)` already does this\") is usually enough.\n- Don't lift informal or coarse phrasing from external sources (blog posts, issues, comments) into the maintainer voice, even in quotes. Paraphrase the substance — quoted-in-context still reads as the maintainer talking.\n- Use prose with inline backticks for symbols. Reach for fenced code blocks only when showing non-trivial code is genuinely clearer than describing it.\n- Skip emojis in substantive technical writeups. A small `👍` in a casual closer is good — it keeps a decline or sign-off sounding warm and friendly without leaning on praise.\n- Bot mentions are bare imperatives: `@pullfrog review`, `@pullfrog fix merge conflicts`, `@pullfrog re-review fresh.`\n- When pushing a follow-up on top of a contributor's PR, state what changed, why it differs from the original approach, and that the maintainer is merging. Never ask the contributor to review the maintainer's changes — they are final, not a proposal. Don't thank them for \"letting\" the maintainer rewrite their work.\n- When posting comments with code samples via `gh`, do NOT pass the body inline through a heredoc that requires escaping backticks. Backslash-escaped backticks (`` \\` ``) inside a `$(cat <<'EOF' ... EOF)` body get sent to GitHub literally and break inline code and template literals inside fenced blocks. Instead, write the comment to a file and pass it via `--body-file <path>` (for `gh pr/issue comment`) or `-F body=@<path>` (for `gh api`). This preserves backticks and `${...}` exactly as written.\n\n## Pushing to a new branch (don't accidentally push to main)\n\nWhen the user asks for a \"new PR\" or \"new branch\", the work has to land on a non-`main` ref on the remote. The footgun: `git worktree add <path> -b <branch> origin/main` (and `git checkout -b <branch> origin/main`) silently set the new branch's upstream to `refs/heads/main` because the start point is a remote-tracking ref. A subsequent `git push -u origin <branch>` then pushes to `refs/heads/main`, not to a new remote branch. Two of these in a row have happened.\n\nAvoid by being explicit on the first push:\n\n```bash\n# Always use a refspec on the initial push so the remote ref name is unambiguous.\ngit push -u origin <branch>:refs/heads/<branch>\n```\n\nThen verify the output. The line you want to see is:\n\n```\n* [new branch]      <branch> -> <branch>\n```\n\nIf the right-hand side says `main`, the push went to main — abort or revert.\n\nUse `git push origin HEAD:refs/heads/<branch>` for subsequent pushes if you didn't `-u` originally. Don't rely on `git push -u origin <branch>` alone — its behavior depends on the upstream config, which `worktree add -b ... origin/main` set wrong for you.\n","CLAUDE.md":"AGENTS.md","packages/bench/typia-case/README.md":"# typia case\n\ntypia generates its validators with a TypeScript transformer, so this directory holds a prebuilt artifact rather than a source import. `build/index.cjs` is generated from `src/index.ts`.\n\nThe transformer does not run inside this workspace: `tspc` live mode crashes on typia's file transformer, and the alternative — `ts-patch install` — patches the TypeScript that every package here shares. The artifact is therefore built in a throwaway project and copied in. The generated file inlines every check, so it depends only on the `typia` runtime already in this package.\n\nTo regenerate:\n\n```sh\nmkdir -p /tmp/typia-build/src && cd /tmp/typia-build\nnub init -y --js --no-install --no-git\nnub add typia@9.7.2 typescript@5.9.3 ts-patch@3.3.0\ncp <repo>/packages/bench/typia-case/src/index.ts src/index.ts\ncp <repo>/packages/bench/typia-case/tsconfig.json tsconfig.json\nnub exec --node ts-patch install && nub exec --node tsc -p tsconfig.json\ncp build/index.js <repo>/packages/bench/typia-case/build/index.cjs\n```\n","packages/docs/.vscode/settings.json":"{\n  \"editor.defaultFormatter\": \"biomejs.biome\",\n  \"editor.formatOnSave\": true,\n  \"editor.codeActionsOnSave\": {\n    \"source.organizeImports.biome\": \"explicit\",\n    \"source.fixAll\": \"always\",\n    \"quickfix.biome\": \"explicit\"\n  },\n  \"editor.wordWrap\": \"wordWrapColumn\",\n  \"editor.wordWrapColumn\": 120,\n  \"[mdx]\": {\n    \"editor.defaultFormatter\": \"unifiedjs.vscode-mdx\",\n    \"editor.formatOnSave\": true\n  }\n}\n","packages/docs/README.md":"# onepager-fuma\n\n<Tabs groupId=\"lib\" items={[\"Zod\", \"Zod Mini\"]}>\n<Tab value=\"Zod\">\n</Tab>\n<Tab value=\"Zod Mini\">\n</Tab>\n</Tabs>\n","packages/docs/content/meta.json":"{\n  \"root\": true,\n  \"pages\": [\n    \"---Documentation---\",\n    \"index\",\n    \"basics\",\n    \"api\",\n    \"error-customization\",\n    \"error-formatting\",\n    \"metadata\",\n    \"json-schema\",\n    \"codecs\",\n    \"compile\",\n    \"ecosystem\",\n    \"library-authors\",\n    \"---Packages---\",\n    \"packages/zod\",\n    \"packages/mini\",\n    \"packages/core\"\n  ]\n}\n","packages/docs/package.json":"{\n  \"name\": \"@zod/docs\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"nub exec --node next build --turbopack\",\n    \"dev\": \"nub exec --node next dev\",\n    \"start\": \"nub exec --node next start\",\n    \"postinstall\": \"nub exec --node fumadocs-mdx\"\n  },\n  \"dependencies\": {\n    \"@inkeep/cxkit-react\": \"^0.5.117\",\n    \"@radix-ui/react-accordion\": \"^1.2.4\",\n    \"@vercel/analytics\": \"^1.5.0\",\n    \"@vercel/og\": \"^0.6.8\",\n    \"fumadocs-core\": \"15.1.0\",\n    \"fumadocs-mdx\": \"11.5.6\",\n    \"fumadocs-ui\": \"15.1.0\",\n    \"gray-matter\": \"^4.0.3\",\n    \"lucide-react\": \"^0.483.0\",\n    \"next\": \"15.5.25\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-tweet\": \"^3.3.1\",\n    \"remark\": \"^15.0.1\",\n    \"remark-gfm\": \"^4.0.1\",\n    \"remark-mdx\": \"^3.1.0\",\n    \"remark-stringify\": \"^11.0.0\",\n    \"shiki\": \"^3.8.1\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"^4.2.4\",\n    \"@types/mdx\": \"^2.0.13\",\n    \"@types/node\": \"22.13.10\",\n    \"@types/react\": \"^19.0.10\",\n    \"@types/react-dom\": \"^19.0.4\",\n    \"postcss\": \"^8.5.12\",\n    \"tailwindcss\": \"^4.2.4\",\n    \"typescript\": \"^5.8.2\"\n  }\n}\n","packages/docs/tsconfig.json":"{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"paths\": {\n      \"@/.source\": [\"./.source/index.ts\"],\n      \"@/*\": [\"./*\"]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"types\": [\"mdx\", \"node\", \"react\", \"react-dom\", \"@vercel/og\"]\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\", \"components/if.tsx\", \"types/**/*.d.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n","packages/docs/tsconfig.test.json":"{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"types\": [\"node\", \"vitest\"]\n  },\n  \"include\": [\"**/*.test.ts\"]\n}\n","packages/docs/vercel.json":"{\n  \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n  \"installCommand\": \"bash -o pipefail -c 'curl -fsSL https://raw.githubusercontent.com/nubjs/nub/0ae8783f1f93763c56dfc827892cfb679e8a0a77/install.sh | bash -s -- 0.8.3 && export PATH=\\\"$HOME/.nub/bin:$PATH\\\" && cd ../.. && nub install --frozen-lockfile'\",\n  \"buildCommand\": \"export PATH=\\\"$HOME/.nub/bin:$PATH\\\" && nub run build\"\n}\n","packages/mini/README.md":"# @zod/mini\n\nZod Mini as a standalone package. It re-exports [`zod/mini`](https://zod.dev/packages/mini) from a peer dependency on `zod`, so every schema it creates is a `zod` schema.\n\n```sh\nnpm install zod @zod/mini\n```\n\n```ts\nimport * as z from \"@zod/mini\";\n\nconst User = z.object({\n  name: z.string(),\n  age: z.number().check(z.int(), z.positive()),\n});\n\nz.parse(User, { name: \"Colin\", age: 30 });\n```\n\nThe version tracks `zod`: each `@zod/mini@4.x.y` requires `zod@^4.x.0`.\n","packages/resolution/README.md":"# Package resolution checks\n\nRun from the repository root:\n\n```sh\nnub run build\nnub run --filter @zod/resolution test:all\n```\n\nThese checks inspect the published declarations with Are the Types Wrong and execute the built CommonJS and ES modules on plain Node through Nub.\n","packages/tsc/README.md":"# Zod perftesting\n\nRun `nub run generate` to generate random Zod schemas.\n\nRun `nub run build:bench` to run `tsc` with `extendedDiagnostics`.\n\nThe workspace dependency points to the local Zod package. Rebuild Zod after changing its typings.\n","packages/zod/README.md":"<p align=\"center\">\n  <img src=\"logo.svg\" width=\"200px\" align=\"center\" alt=\"Zod logo\" />\n  <h1 align=\"center\">Zod</h1>\n  <p align=\"center\">\n    TypeScript-first schema validation with static type inference\n    <br/>\n    by <a href=\"https://x.com/colinhacks\">@colinhacks</a>\n  </p>\n</p>\n<br/>\n\n<p align=\"center\">\n<a href=\"https://github.com/colinhacks/zod/actions?query=branch%3Amain\"><img src=\"https://github.com/colinhacks/zod/actions/workflows/test.yml/badge.svg?event=push&branch=main\" alt=\"Zod CI status\" /></a>\n<a href=\"https://opensource.org/licenses/MIT\" rel=\"nofollow\"><img src=\"https://img.shields.io/github/license/colinhacks/zod\" alt=\"License\"></a>\n<a href=\"https://www.npmjs.com/package/zod\" rel=\"nofollow\"><img src=\"https://img.shields.io/npm/dw/zod.svg\" alt=\"npm\"></a>\n<a href=\"https://discord.gg/KaSRdyX2vc\" rel=\"nofollow\"><img src=\"https://img.shields.io/discord/893487829802418277?label=Discord&logo=discord&logoColor=white\" alt=\"discord server\"></a>\n<a href=\"https://github.com/colinhacks/zod\" rel=\"nofollow\"><img src=\"https://img.shields.io/github/stars/colinhacks/zod\" alt=\"stars\"></a>\n</p>\n\n<div align=\"center\">\n  <a href=\"https://zod.dev/api\">Docs</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://discord.gg/RcG33DQJdf\">Discord</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://twitter.com/colinhacks\">𝕏</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://bsky.app/profile/zod.dev\">Bluesky</a>\n  <br />\n</div>\n\n<br/>\n<br/>\n\n### [Read the docs →](https://zod.dev/api)\n\n<br/>\n<br/>\n\n## What is Zod?\n\nZod is a TypeScript-first validation library. Define a schema and parse some data with it. You'll get back a strongly typed, validated result.\n\n```ts\nimport * as z from \"zod\";\n\nconst User = z.object({\n  name: z.string(),\n});\n\n// some untrusted data...\nconst input = {\n  /* stuff */\n};\n\n// the parsed result is validated and type safe!\nconst data = User.parse(input);\n\n// so you can use it with confidence :)\nconsole.log(data.name);\n```\n\n<br/>\n\n## Features\n\n- Zero external dependencies\n- Works in Node.js and all modern browsers\n- Tiny: `2kb` core bundle (gzipped)\n- Immutable API: methods return a new instance\n- Concise interface\n- Works with TypeScript and plain JS\n- Built-in JSON Schema conversion\n- Extensive ecosystem\n\n<br/>\n\n## Installation\n\n```sh\nnpm install zod\n```\n\n<br/>\n\n## Basic usage\n\nBefore you can do anything else, you need to define a schema. For the purposes of this guide, we'll use a simple object schema.\n\n```ts\nimport * as z from \"zod\";\n\nconst Player = z.object({\n  username: z.string(),\n  xp: z.number(),\n});\n```\n\n### Parsing data\n\nGiven any Zod schema, use `.parse` to validate an input. If it's valid, Zod returns a strongly-typed _deep clone_ of the input.\n\n```ts\nPlayer.parse({ username: \"billie\", xp: 100 });\n// => returns { username: \"billie\", xp: 100 }\n```\n\n**Note** — If your schema uses certain asynchronous APIs like `async` [refinements](https://zod.dev/api#refinements) or [transforms](https://zod.dev/api#transforms), you'll need to use the `.parseAsync()` method instead.\n\n```ts\nconst schema = z.string().refine(async (val) => val.length <= 8);\n\nawait schema.parseAsync(\"hello\");\n// => \"hello\"\n```\n\n### AOT compilation\n\nFor hot validation paths, `z.compile(schema)` returns a schema clone with an ahead-of-time compiled fast path. Valid inputs take the compiled path; invalid inputs fall back to the regular parser so error reporting stays identical.\n\nAcross a 55-schema benchmark the median speedup is **2.4x**, and it scales with how much work the schema does per parse: a large array of objects is ~9x, a 20-key object ~9x, a nested object ~4.5x, while a bare `z.string()` gains nothing — compilation removes per-node dispatch and allocation, and a single `typeof` has none to remove.\n\n```ts\nconst CompiledPlayer = z.compile(Player);\n\nCompiledPlayer.parse({ username: \"billie\", xp: 100 });\n```\n\nTo enable compilation globally for schemas constructed after import:\n\n```ts\nimport \"zod/compile\"; // place before modules that define schemas\n```\n\nThings to know:\n\n- Compilation uses `new Function`. Global mode is automatically disabled when `z.config({ jitless: true })` is set (e.g. CSP environments); calling `z.compile()` directly is an explicit opt-in.\n- Schemas with async refinements or transforms can't be compiled, and neither can a few other constructs. That is not an error: `z.compile()` hands the schema back unchanged and it keeps using the regular parser, exactly as global mode leaves it. Pass `{ strict: true }` to throw `ZodCompileAsyncError` / `ZodCompileUnsupportedError` instead.\n- On invalid input, refinements and transforms may run twice (fast path, then fallback).\n- Deriving a new schema from a compiled one (`.refine()`, `.extend()`, etc.) returns an uncompiled schema — compile the final schema.\n\nSee [`compile` docs](https://zod.dev/compile) for details.\n\n### Handling errors\n\nWhen validation fails, the `.parse()` method will throw a `ZodError` instance with granular information about the validation issues.\n\n```ts\ntry {\n  Player.parse({ username: 42, xp: \"100\" });\n} catch (err) {\n  if (err instanceof z.ZodError) {\n    err.issues;\n    /* [\n      {\n        expected: 'string',\n        code: 'invalid_type',\n        path: [ 'username' ],\n        message: 'Invalid input: expected string, received number'\n      },\n      {\n        expected: 'number',\n        code: 'invalid_type',\n        path: [ 'xp' ],\n        message: 'Invalid input: expected number, received string'\n      }\n    ] */\n  }\n}\n```\n\nTo avoid a `try/catch` block, you can use the `.safeParse()` method to get back a plain result object containing either the successfully parsed data or a `ZodError`. The result type is a [discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions), so you can handle both cases conveniently.\n\n```ts\nconst result = Player.safeParse({ username: 42, xp: \"100\" });\nif (!result.success) {\n  result.error; // ZodError instance\n} else {\n  result.data; // { username: string; xp: number }\n}\n```\n\n**Note** — If your schema uses certain asynchronous APIs like `async` [refinements](https://zod.dev/api#refinements) or [transforms](https://zod.dev/api#transforms), you'll need to use the `.safeParseAsync()` method instead.\n\n```ts\nconst schema = z.string().refine(async (val) => val.length <= 8);\n\nawait schema.safeParseAsync(\"hello\");\n// => { success: true; data: \"hello\" }\n```\n\n### Inferring types\n\nZod infers a static type from your schema definitions. You can extract this type with the `z.infer<>` utility and use it however you like.\n\n```ts\nconst Player = z.object({\n  username: z.string(),\n  xp: z.number(),\n});\n\n// extract the inferred type\ntype Player = z.infer<typeof Player>;\n\n// use it in your code\nconst player: Player = { username: \"billie\", xp: 100 };\n```\n\nIn some cases, the input & output types of a schema can diverge. For instance, the `.transform()` API can convert the input from one type to another. In these cases, you can extract the input and output types independently:\n\n```ts\nconst mySchema = z.string().transform((val) => val.length);\n\ntype MySchemaIn = z.input<typeof mySchema>;\n// => string\n\ntype MySchemaOut = z.output<typeof mySchema>; // equivalent to z.infer<typeof mySchema>\n// number\n```\n",".cursorrules (typescript-zod-tailwind-nextjs-cursorrules-prompt-.mdc)":"---\ndescription: \"Cursor rules for TypeScript development with Zod, Tailwind, and Next.js integration.\"\nglobs: **/*\nalwaysApply: false\n---\n# Coding Style Guide\n\nCode Style and Structure:\n- Write concise, technical TypeScript code with accurate examples\n- Use functional and declarative programming patterns; avoid classes\n- Prefer iteration and modularization over code duplication\n- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError)\n- Structure files: exported component, subcomponents, helpers, static content, types\n\nNaming Conventions:\n- Use lowercase with dashes for directories (e.g., components/auth-wizard)\n- Favor named exports for components\n\nTypeScript Usage:\n- Use TypeScript for all code; prefer interfaces over types\n- Avoid enums; use maps instead\n- Use functional components with TypeScript interfaces\n- Use Zod for form validation\n\nSyntax and Formatting:\n- Use the \"function\" keyword for pure functions\n- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements\n- Use declarative JSX\n\nError Handling and Validation:\n- Prioritize error handling: handle errors and edge cases early\n- Use early returns and guard clauses\n- Implement proper error logging and user-friendly messages\n- Use Zod for form validation\n- Model expected errors as return values in Server Actions\n- Use error boundaries for unexpected errors\n\nUI and Styling:\n- Use Shadcn UI, Radix, and Tailwind Aria for components and styling\n- Implement responsive design with Tailwind CSS; use a desktop-first approach\n\nPerformance Optimization:\n- Minimize 'useEffect', and 'setState'; favor React Remix Components (RSC)\n- Wrap client components in Suspense with fallback\n- Use dynamic loading for non-critical components\n- Optimize images: use WebP format, include size data, implement lazy loading\n\nKey Conventions:\n- Use proper URL search parameter state management\n- Optimize Web Vitals (LCP, CLS, FID)\n- Limit 'use client'\n\nWhen React Server Components (RSC) are used:\n- Favor server components and Next.js SSR\n- Use only for Web API access in small components\n- Avoid for data fetching or state management\n\nFollow React Remix docs for Data Fetching, Rendering, and Routing\n\nFollow Next.js docs for Data Fetching, Rendering, and Routing when Next JS is used instead of React Remix\n"},"files":{".claude/skills/security-advisory/SKILL.md":"---\nname: security-advisory\ndescription: Triage a draft security advisory in colinhacks/zod — a private vulnerability report from the Security tab, named by GHSA id (GHSA-xxxx-xxxx-xxxx) or reached from \"the draft advisories\", \"the security reports\", \"the vulnerability queue\". Use instead of the `triage` skill whenever the ticket is an advisory rather than a public issue or PR, because the workflow is different: the report is private, the burden of proof is on the reporter, the fix must be LANDED before any comment is drafted, and the publish-or-decline call has its own calculus. Covers reproducing a PoC against main, the schema-vs-input test, deduplicating a cluster of reports into distinct defects, landing the fix first, the publish-vs-decline framework, per-reporter privacy rules that make a GHSA id unlinkable, and the results.md format under .triage/advisories/GHSA-xxxx-xxxx-xxxx/.\n---\n\n# Triaging a draft security advisory\n\nAdvisories are not issues with a scarier label. Four things differ, and each one changes the order you work in:\n\n- **The report is private**, and private in both directions — see `## Privacy cuts both ways`.\n- **The burden of proof is on the reporter.** Most submissions are scanner output or a rediscovery of intended behavior. A plausible writeup is not evidence.\n- **The fix lands before the comment exists.** See `## Land the fix first`. This is the rule most often broken and the one that wastes the most of Colin's time.\n- **Real defect and publishable vulnerability are different questions.** See `## Publish or decline`.\n\nEverything in the `triage` skill about where files live, `results.md` frontmatter, close-comment voice, and reporting back in chat still applies. This skill replaces only its `## Investigating a draft advisory` section.\n\n**Never accept, publish, decline, or comment on an advisory.** The write-up and the drafted note are the deliverables; the disposition is Colin's. Landing a fix is a code change and follows the repo's normal rules; publishing a CVE is not, and is never yours.\n\n## Reading the queue\n\n```bash\ngh api repos/colinhacks/zod/security-advisories --paginate \\\n  --jq '.[] | select(.state==\"triage\") | [.ghsa_id, .severity, .summary] | @tsv'\ngh api repos/colinhacks/zod/security-advisories/<GHSA-ID> --jq '.description'   # full report + PoC\n```\n\nWrite-ups go to `.triage/advisories/<GHSA-ID>/results.md`, scratch files beside them. Frontmatter swaps `number:` for `ghsa:` and adds `severity_claimed:` (what the reporter asserted) next to `severity_actual:` (yours, or `none`).\n\n`verdict` is one of `valid`, `not-a-vulnerability`, `already-fixed`, `duplicate` (with `duplicate_of:`), `needs-reporter-info`, `needs-maintainer-decision`. Add `fixed_by:` / `fix_pr:` once a fix lands, and `disposition_recommended:` when you have a publish-or-decline opinion. Keep `verdict` describing the defect and `disposition_recommended` describing what to do about it — do not let a decline quietly rewrite a real finding as `not-a-vulnerability`.\n\n## Order of work\n\n1. **Extract the PoC verbatim** into `.triage/advisories/<GHSA-ID>/poc.ts`. Do not clean it up first — a PoC that only works after you fix it is a finding in itself.\n2. **Reproduce against current `main`**: `nub run dev .triage/advisories/<GHSA-ID>/poc.ts`. If it does not reproduce, say so and check whether it ever did (`git log -S`, or test the version the reporter named).\n3. **Decide whether the reproduced behavior is a vulnerability at all.** The recurring question on this repo is whether the attacker controls the **schema** or only the **input**. A schema is application code, so a report needing an attacker-authored schema is usually `not-a-vulnerability`. For prototype reports: is the polluted prototype the returned object's own (contained) or `Object.prototype` itself (real)?\n4. **Enumerate reachability yourself; do not inherit the reporter's.** Reporters test the path they found. Grep every call site that can feed attacker data into the sink and build a route table — precondition per route, and whether each needs only input, or a particular schema shape, or cooperating application code. This is where the severity actually gets decided, and it routinely turns up both routes the reporter missed and routes that are already closed.\n5. **Dedupe the cluster.** Many reports describe one defect. `grep -rl '<keyword>' .triage/advisories/*/results.md`, pick a canonical, cross-link the rest with `duplicate_of`. Fold by underlying defect, never by title keyword.\n6. **Check for public prior art** — an open issue or PR describing the same bug without the security framing. If one exists the defect is already public, which kills the embargo and changes the disposition.\n7. **Land the fix** — see below.\n8. **Then** write `results.md` and draft the reporter note.\n\n## Land the fix first\n\nA close comment's entire value is the line that says where it was fixed. Draft one before a fix exists and you have written a decline, whatever it says — the reporter gets no commit to verify, no release to wait for, and no reason to believe the report was acted on. Colin then has to do the work you were asked to do before he can send anything.\n\nSo the fix is not a follow-up to the triage. It is a step inside it, and it comes before any comment is drafted:\n\n- **Search for existing PRs before writing one.** Popular defects attract near-duplicate community PRs — this repo had seven for a single formatter bug. Read every diff, rank them, and take the best as a base rather than opening an eighth.\n- **Judge the candidates against the whole defect, not the reported symptom.** A patch that fixes the one function in the PoC while three sibling call sites keep the bug is partial. Build a behavior matrix of the cases that matter, run it against `main` and against each candidate patch, and let that table pick the winner. This is also how you catch a patch that trades a crash for silent data loss.\n- **Extend the base rather than starting over**, and preserve the contributor's commits — AGENTS.md covers pushing to a contributor's head ref.\n- **Run what CI runs** before pushing, then confirm CI itself is green.\n- **Do not bump a version and do not cut a release.** Landing on `main` is the goal. Release timing on a security fix is Colin's, and on this repo approving the release workflow's `npm` environment is the one irreversible action.\n\nOnly once the fix is merged do you write the comment, and the comment names the merge commit and the PR.\n\nIf the fix cannot land — the right change is a breaking one, or the affected major is EOL — say so explicitly in `results.md` and draft the comment around what the reporter's actual remedy is. That is a real outcome, not a skipped step.\n\n## Privacy cuts both ways\n\nThe report body may contain an unpatched exploit, so it never goes into a public issue, PR, or commit message. A fix can land publicly while the advisory stays private — describe the defect neutrally in the commit and let the mechanism speak for itself.\n\nThe direction that gets missed: **each reporter can see only their own advisory thread.** A GHSA id in a comment is a dead link to everyone but the maintainers. A duplicate close must never say \"duplicate of GHSA-xxxx\" or \"see the reasoning on that thread\" — the reporter can open neither. Every comment stands alone: state the reasoning inline, and refer to a sibling report as \"a separate report of the same defect\", without the id and without the other reporter's name. Public issues, PRs and commit SHAs are the only cross-references a reporter can follow. The `duplicate_of` link belongs in `results.md`, which is for Colin.\n\n## Publish or decline\n\nA defect can be real and still not be worth a published GHSA. Answer these before recommending either way:\n\n- **Does the attacker control the value, or only the key?** A write the attacker cannot choose the value of — a fixed object, a truthiness flip — needs a second-order sink in the victim's own code before it does anything. That sink is the application's bug, not the library's.\n- **What actually happens at the boundary?** Test the near-miss cases rather than assuming. A gadget that throws on the interesting inputs and only pollutes on the boring ones is much weaker than the report claims.\n- **Is the supported major reachable without cooperating application code?** If the current version needs an unusual schema shape or an app that derives paths from user input, an advisory naming it overstates the exposure.\n- **Is there a patch to point at?** An advisory against an EOL major with no patch turns every install red in `npm audit`, mostly transitively, with \"upgrade the major\" as the only remedy. That is a large ecosystem cost for little defensive gain.\n- **Is it already public?** A defect with an open issue describing it was never embargoable. Fix it in the open.\n\nState the recommendation plainly and name what would reverse it. Where a reachability judgment rests on how common some application pattern is, say that you reasoned it rather than measured it.\n\nCredit is the thing to offer when declining. A reporter who found a real bug that produced a real fix wants attribution — release-notes credit costs nothing and is usually what they were after.\n\n## The reporter note\n\nPer the `triage` skill's `## Drafting the close comment`, plus one advisory-specific pressure: a decline invites you to walk the reporter through the entire severity argument, and the analysis is right there. Do not paste it. Four things belong in the note — it is fixed and where, whether you are publishing, the single reason that decides it, and the offer of credit. Everything else lives in `results.md`.\n\nCorrect a reporter's overstated impact when it is wrong, in one sentence, with what you actually observed. Do not relitigate their CVSS vector line by line.\n",".claude/skills/triage/SKILL.md":"---\nname: triage\ndescription: Investigate a GitHub issue or pull request in colinhacks/zod and write up a durable verdict. Use whenever asked to triage, investigate, review, evaluate, or form an opinion on an issue or PR (by number, URL, or \"the open PR queue\"), and when sweeping many of them in bulk. For a draft SECURITY ADVISORY (a GHSA id, the Security tab, a private vulnerability report) use the `security-advisory` skill instead — it shares this file's conventions but its workflow lands the fix before drafting any comment. Covers where write-ups live on disk (.triage/issues/NNNN/, .triage/prs/NNNN/), the cheap-disqualifier pass that avoids spinning up a worktree for stale PRs, the worktree checkout procedure for PRs, the results.md format that keeps a 250-PR sweep greppable and resumable, how to report back in chat — the final message is all the user sees, so it leads with a code block showing the problem and a short outline of the fix — and the maintainer-voice comment it drafts whenever the verdict is to close.\n---\n\n# Investigating issues and PRs\n\nProduce a write-up a maintainer can act on without re-reading the thread. The durable deliverable is always a file on disk — sweeps span many sessions and agents, so anything living only in a transcript is lost. But the file is not what the user reads first: your final chat message is, and for most tickets it is the only thing they will ever read. Both matter, and `## Reporting back` below is not optional.\n\n**Read-only on GitHub.** Never comment, label, assign, close, review, approve, or merge unless the user explicitly asks in this session. You are forming an opinion, not publishing one. Drafting the close comment described below is part of forming it — writing the text is required, sending it is not yours to do.\n\n## Where things live\n\nEverything goes under `.triage/` in the **root repo**, which is gitignored — write freely, it never shows up in `git status`.\n\n```\n.triage/\n  index.md                    # the original issue sweep index — hand-maintained prose, still the canonical issue queue\n  issues/index.md             # generated from frontmatter by reindex.mjs\n  issues/<N>/results.md       # the write-up; scratch files live beside it\n  prs/index.md                # generated\n  prs/<N>/results.md\n  advisories/<GHSA-ID>/results.md   # draft security advisories from the Security tab\n  signal-report.md            # generated by scripts/triage-signal.ts — ranks the open backlog\n```\n\nOne directory per ticket, named by bare GitHub number (no leading zeros, no `issue-`/`pr-` prefix) — or by bare GHSA id for an advisory. `results.md` is the write-up. Everything else you need — repro scripts, a saved `gh pr diff`, test fixtures, benchmark output, notes — goes in that same directory. Name scratch files descriptively (`repro.ts`, `diff.patch`, `bench-before.txt`); there is no naming rule beyond not colliding with `results.md`.\n\nFor an issue repro, `nub run dev .triage/issues/<N>/repro.ts` from the root repo works directly.\n\nA repro must sit **inside** the checkout it is testing, because module resolution walks up from the file, not from the cwd — a script under `.triage/` cannot resolve `zod` from a PR worktree. Keep the canonical copy in the ticket directory and copy it in to run: `cp .triage/prs/<N>/repro.ts <worktree>/repro-<N>.ts`. That is also what lets you run the identical file against both `main` and the PR and diff the output, which is the cheapest way to characterize a behavior change. For a probe needing GC or other flags, invoke Nub with the flags directly: `nub --expose-gc --conditions=@zod/source repro-<N>.ts`.\n\n### Writing to the root repo from a PR worktree\n\nThis is the one mistake that silently destroys work. A worktree has its **own** gitignored `.triage/`, so a relative write from inside it lands somewhere nobody will ever look.\n\nResolve the root once and always write through it:\n\n```bash\nZOD_ROOT=$(git worktree list | head -1 | awk '{print $1}')   # correct from inside any worktree\nmkdir -p \"$ZOD_ROOT/.triage/prs/<N>\"\n```\n\nEvery path you write must start with `$ZOD_ROOT`. Investigate in the worktree, record in the root.\n\n## results.md format\n\nOpen with YAML frontmatter. It is what makes a 250-PR sweep aggregatable and resumable — an interrupted sweep is resumed by grepping `status`, never by re-reading write-ups.\n\n```yaml\n---\nnumber: 5928\ntype: pr                    # pr | issue\nstatus: done                # in-progress | done\nverdict: merge-with-changes\ntitle: \"fix: preserve brand through .optional()\"\nauthor: someuser\nurl: https://github.com/colinhacks/zod/pull/5928\ninvestigated: 2026-08-08\n---\n```\n\n`verdict` for **PRs** — one of:\n\n- `merge-as-is` — correct, tested, in scope. Say so plainly.\n- `merge-with-changes` — right idea, needs specific edits. Enumerate them concretely enough to apply.\n- `needs-author-changes` — blocked on something only the author can supply (repro, rebase, scope cut).\n- `decline` — out of scope or wrong approach. Give the one or two strongest reasons, plus the escape hatch users actually have.\n- `superseded` — already fixed on `main` or landed via another PR. Name the commit or PR.\n- `needs-maintainer-decision` — the code is fine; the call is a product/API judgment that is Colin's to make. State the tradeoff and your recommendation.\n\n`verdict` for **issues** — reuse the vocabulary already established in `.triage/index.md`: `good-idea`, `already-fixed`, `needs-maintainer-input`, `not-recommended`, `needs-more-reproduction`, `duplicate` (add a `duplicate_of: NNNN` key alongside it).\n\n`verdict` for **advisories** — one of:\n\n- `valid` — a real vulnerability in zod, reproducible on current `main`. State the affected versions and the fix.\n- `not-a-vulnerability` — the behavior is intended, requires the app to already be doing something unsafe, or is a plain bug with no security consequence. This is the most common outcome; say which.\n- `already-fixed` — real, but no longer reproduces on `main`. Name the commit or release that fixed it.\n- `duplicate` — the same defect as another advisory (add `duplicate_of: GHSA-xxxx-xxxx-xxxx`). The `__proto__` reports cluster heavily; check before concluding.\n- `needs-reporter-info` — the report is too vague to reproduce, or the PoC does not run.\n- `needs-maintainer-decision` — real behavior, but whether it counts as a vulnerability is a threat-model call that is Colin's to make.\n\nAdvisory frontmatter swaps `number:` for `ghsa:` and adds `severity_claimed:` (what the reporter asserted) alongside `severity_actual:` (your assessment, or `none`).\n\nSet `status: in-progress` in a stub the moment you start, so a parallel agent or a later session does not duplicate the work. Flip to `done` before you report back.\n\n**One pass. Finish the ticket.** A maintainer question is never a reason to leave a write-up unfinished — investigate fully, commit to a recommended `verdict`, write every section including `## Proposed comment`, and flip `status: done`. Anything genuinely needing the maintainer goes in `## Open questions` (below) *inside the finished file*, so their answer is a one-line amendment to a complete write-up rather than a second investigation. Never hand back a provisional verdict, a `status: in-progress` file, or a promise to finalize later; never wait on an answer before writing. `verdict: TBD` is not a verdict. The only thing that legitimately blocks `done` is evidence you could not obtain — a PR that will not build, a repro that needs credentials — and that is `needs-author-changes` or `needs-more-reproduction`, which are verdicts, not deferrals.\n\nAfter the frontmatter, write prose under these headings. Match the density of the existing issue write-ups in `.triage/issues/*/results.md` — they are the house style; skim one before your first write-up.\n\n- `## Request` / `## Change` — what is actually being asked or changed, in your own words. Include the thread's evolution when maintainers or the author already moved the discussion.\n- `## Current state` — what the code on `main` does **today**, with `file.ts:line` references. Most old tickets were filed against v3 or early v4 and are stale on arrival; this section is usually where the verdict comes from.\n- `## Analysis` — correctness, edge cases, type-level implications, perf, and whether the tests actually cover the claim.\n- `## Recommendation` — the action to take, and the substance of the reply to post. Do not draft it in maintainer voice unless asked, with one standing exception below.\n- `## Proposed comment` — **required whenever the recommendation is to close the ticket**, in a fenced block, ready to paste. See below.\n- `## Open questions` — **only** for calls that are genuinely the maintainer's: product/API direction, a breaking-change tradeoff, taste, or picking between two contributors' PRs. Omit the heading entirely when there are none, which is the common case. Never park a question here that the code, the tests, `git log`, or an existing write-up answers — that is research you owe, not a decision they owe. See below.\n\n### Writing `## Open questions`\n\nEach question is answerable cold, by someone who has not read the write-up and will not open it. One line for what is at stake, then 2–4 lettered options, each with a one-line consequence, with your recommendation marked and placed first. Write in behavior, not identifiers: \"should a schema that declares a field named `__proto__` still reject it?\" not \"should `handleCatchall` precede the `keySet` check?\".\n\nThe verdict above already assumes your recommended option. Say which verdict each *other* option would produce, so the maintainer can see the cost of disagreeing without asking you to redo the analysis:\n\n```markdown\n## Open questions\n\n**Q1 — Should `.strict()` reject an own `__proto__` key, or keep silently dropping it?** Runtime breaking change for anyone posting JSON bodies through `.strict()`.\n\n- **(A) Report it — take the PR.** ← recommended. Restores \"strict rejects every unknown key\". Verdict as written: `merge-with-changes`.\n- (B) Keep dropping it, document the exemption. Zero breakage; the one key an attacker actually sends stays the one key strict does not flag. → `decline`.\n- (C) Report it behind a config flag. No breakage; a permanent knob for a two-line behavior. → `needs-author-changes`.\n```\n\nWhen the answer arrives, amend the file in place: set the final `verdict`, fold the decision into `## Recommendation`, rewrite `## Proposed comment` to match, and leave the question and its answer recorded so nobody relitigates it. `status` stays `done` throughout — it was never not done.\n\n### Drafting the close comment\n\nAny verdict whose action is \"close this\" gets a drafted reply: `decline` and `superseded` on a PR, `not-recommended`, `already-fixed`, and `duplicate` on an issue, and `not-a-vulnerability`, `already-fixed`, and `duplicate` on an advisory. Closing is the one outcome where a human is going to have to say something to a real person who spent time on this, so writing that sentence is part of the triage, not a follow-up task.\n\nFollow the \"Commenting on issues and PRs\" section of AGENTS.md for the maintainer voice. Succinct, conversational, friendly. Lead with the decision, give the one or two strongest reasons, name the escape hatch the user actually has, and close warmly. Two or three sentences is usually the whole thing.\n\nFor an issue reporting that `z.coerce.number()` accepts `null`, the whole draft is:\n\n```markdown\nWorking as intended. `z.coerce.number()` runs `Number()` on the input before it validates, and `Number(null)` is `0` — the coercion happens first, so there is nothing left to reject by the time the number check runs.\n\nIf you want these rejected, use plain `z.number()` and do the coercion at the boundary where you actually know the input is a numeric string. Closing this out — thanks for the writeup 👍\n```\n\nNote what it does not do: no bullet list of every objection, no apology, no \"great question\", no asking whether the reporter agrees. Decision, reason, escape hatch, closer.\n\n#### The investigation's depth never sets the comment's length\n\nThis is the rule that gets broken, and it gets broken hardest on exactly the tickets where the investigation went deepest. A week of benchmarks, a prior-art trail, a prototype, an ecosystem comparison — all of it feels load-bearing, so it leaks into the reply and the two-or-three-sentence bar quietly becomes five paragraphs. **The evidence you are proudest of is the first thing to cut.** It belongs in `results.md`, which is where a maintainer looks when someone pushes back; the comment is not the place to prove you did the work.\n\nA worked case. A draft advisory reported ReDoS in `z.fromJSONSchema()` — real, reproducible, two sinks, exponential. The investigation ran long: measured timings, prior art in a closed issue and a closed fix PR, a 54MB dependency weighed, a zero-dep detector prototyped and evaluated, Ajv's published security contract pulled in as precedent. The drafted comment carried most of that. What the maintainer actually posted, in full:\n\n```markdown\nClosing this out — declining. `z.fromJSONSchema` is designed to convert arbitrary JSON Schema to a Zod schema, and it up to the user to do their own out of band validation. Thanks for the report.\n```\n\nThree sentences, and **this is what most declining comments should look like.** Read what it drops and adopt each as a rule:\n\n- **No cross-references.** No issue number, no PR number, no prior-art trail — even when a maintainer closed the identical report before. That history justifies the decision to *you*; it does not need to be shown to the reporter.\n- **No numbers, no engineering.** No timings, no dependency sizes, no analysis of what a guard would cost or why the cheap version fails. None of it changes the answer.\n- **No external precedent.** How another library documents the same contract is research that settles your verdict, not material for the reply.\n- **The reason is design intent, asserted — not an argument, defended.** \"Is designed to convert arbitrary JSON Schema\" states the contract in one clause and stops. A maintainer declares scope; they do not litigate it. Once you find yourself building a case, you have written the wrong document.\n- **No concession and no promise.** Do not offer \"you have a fair point about X\", and do not commit to a docs change, a new option, or any follow-up. Real follow-ups get filed as their own issue; a decline that promises work is no longer a decline.\n\nKeep only: the decision, one clause of reason, what the user does instead (\"their own out of band validation\"), and a short closer. When a drafted comment runs past three or four sentences, that is the signal to cut, not to reorganize.\n\nDrafting is not posting. The draft sits in `results.md` and in your chat message; you never send it unless the user explicitly asks in this session. For an advisory the draft is the note to the reporter, and it must not restate an unpatched exploit.\n\nThe length rule binds hardest on an advisory decline, which is where it is most often broken: a reporter who filed a real, reproducible defect invites you to walk them through the whole severity argument, and the analysis you just spent hours on is right there. Do not paste it. The reporter needs four things — it is fixed and where, you are not publishing, the one reason that actually decides it, and the offer of credit. Everything else belongs in `results.md`. A decline that runs to six paragraphs is a worse decline, not a more rigorous one.\n\n## Reporting back\n\n`results.md` is the archive. **Your final chat message is the interface, and for most tickets it is the only thing the user will ever read.** Assume they see it hours later, in a queue, without the thread in front of them, without opening the file, and without any of your working context. \"Investigated #5928, write-up is in `.triage/prs/5928/results.md`\" has delivered nothing — it just moved the work onto them.\n\nSo the last message before you rest always carries these three things, in this order, plus a fourth when the verdict is to close:\n\n**1. The problem, as a code block.** Not a paragraph describing the code block — the actual minimal snippet, with the wrong result written next to it as a comment. This is the single highest-value thing in the message: it is what lets the user confirm or reject your read in five seconds instead of re-deriving the whole ticket. Strip the reporter's app scaffolding down to the schema and the one call that misbehaves, and show actual versus expected.\n\n```ts\nconst schema = z.coerce.number();\n\nschema.parse(null); // → 0\nschema.parse(\"\"); // → 0\nschema.parse([]); // → 0\n// reporter expected each of these to fail validation\n```\n\nType-level tickets get the same treatment, with the inferred type as a comment instead of the parse result:\n\n```ts\ntype Out = z.infer<typeof schema>;\n// actual:   string | undefined\n// expected: string\n```\n\n**2. The shape of the fix, in two or three sentences.** Where it goes (`file.ts:line`), what changes, and how big it really is — one-line type tweak, new method on `ZodType`, or a refactor that touches every wrapper. If the fix is not worth doing, say what the user does instead. Do not paste the patch; the user is deciding whether to spend time here, not applying it from chat.\n\n**3. The verdict and the path.** The frontmatter `verdict` verbatim, plus a link to the ticket and the `results.md` path for the detail you left out.\n\n**4. The proposed comment, if the verdict is to close.** Repeat the `## Proposed comment` draft in the message, in its own fenced block, so the user can read and paste it without opening the file. Say plainly that you have not posted it.\n\n**5. The open questions, if the write-up has any.** Repeat them from `## Open questions`, options and all, so they are answerable without opening the file. Frame them as what they are — the ticket is triaged and the verdict is recorded; the answer only confirms or redirects it. Never present them as a request for permission to finish, never say the write-up is waiting on them, and never end on \"send me your answer and I'll finalize\". The work is already done.\n\nKeep the whole thing short — the code block plus a few sentences. The message is a summary that stands alone, not a condensed replay of the write-up, and it is not the place for the investigation narrative, the commands you ran, or the branches you ruled out. Those are what the file is for.\n\nTwo adjustments by shape of the task:\n\n- **Nothing reproduced** — an `already-fixed` or `not-a-vulnerability` verdict still gets a code block. Show the reporter's own snippet and the *correct* output it now produces on `main`; that is the evidence, and without it the user just has your word.\n- **A bulk sweep** — one summary table of ticket, verdict, and a one-line reason, and code blocks only for the two or three tickets that genuinely need a decision. Twenty code blocks is the same as none. Every close still gets its drafted comment written into its own `results.md`; the message just says which tickets have one waiting instead of inlining them all.\n- **More than one ticket has a comment to post** — pair each one: the link on its own line, its fenced comment block **directly underneath it**, then the next link. Never collect the links in one place and the comment blocks in another, and never make the user match a block to a link by reading the prose between them. This is how a set of draft advisories is always handed back, since the user posts them one at a time with the link and its text side by side.\n\n  ```markdown\n  ### [GHSA-xxxx-xxxx-xxxx](https://github.com/colinhacks/zod/security/advisories/GHSA-xxxx-xxxx-xxxx) — valid\n\n  <the comment for that advisory, in its own fenced block>\n\n  ### [GHSA-yyyy-yyyy-yyyy](https://github.com/colinhacks/zod/security/advisories/GHSA-yyyy-yyyy-yyyy) — duplicate\n\n  <the comment for that advisory, in its own fenced block>\n  ```\n\n## Investigating an issue\n\nNo worktree — read the code on `main`.\n\n1. `gh issue view <N> -R colinhacks/zod --comments` and read the whole thread, including maintainer replies. A prior decision from Colin usually settles it.\n2. Check for duplicates and prior art: `gh search issues --repo colinhacks/zod '<keywords>'`, and grep `.triage/issues/*/results.md` — the sweep may already cover it. Cross-link with a relative `../NNNN/results.md`.\n3. Reproduce against current source before believing the report. Write the repro to `.triage/issues/<N>/repro.ts` and run `nub run dev` on it. A large share of open issues are already fixed.\n4. Brainstorm before concluding. Consider the fix, its blast radius, whether it belongs in core at all, and what userland escape hatch already exists.\n5. Write `results.md`, then report back per `## Reporting back`.\n\n## Investigating a draft advisory\n\n**Use the `security-advisory` skill instead — invoke it before you start.** Draft advisories live in the Security tab, so nothing in `gh issue list` surfaces them, and the workflow diverges from this one in ways that matter: the report is private in both directions, the burden of proof sits with the reporter, the fix must be **landed before any comment is drafted**, and \"real defect\" and \"publishable vulnerability\" are separate calls.\n\nWhat carries over from this skill: where files live, the `results.md` frontmatter and headings, the close-comment voice, and `## Reporting back`. Advisory write-ups go to `.triage/advisories/<GHSA-ID>/results.md` with `ghsa:` in place of `number:`.\n\nNever accept, publish, decline, or comment on an advisory. The write-up and the drafted note are the deliverables; the disposition is Colin's.\n\n## Investigating a PR\n\nCheap pass first — most of a 250-PR backlog is resolved without ever checking out code.\n\n```bash\ngh pr view <N> -R colinhacks/zod --json number,title,author,state,isDraft,mergeable,mergeStateStatus,baseRefName,headRefName,headRepositoryOwner,maintainerCanModify,createdAt,updatedAt,additions,deletions,changedFiles,labels,comments\ngh pr diff <N> -R colinhacks/zod\n```\n\nDisqualify early and write the write-up straight from this, no worktree, when the diff is small and self-evident, or when the PR targets v3 / duplicates a landed change / is already superseded by `main` / is abandoned with unresolved conflicts. Verify `superseded` against actual code on `main` — do not infer it from dates.\n\nOtherwise check it out (this repo's convention, per AGENTS.md):\n\n```bash\ngit fetch origin pull/<N>/head:pr-<N>\ngit worktree add ~/.cursor/worktrees/zod/pr-<N> pr-<N>\ncd ~/.cursor/worktrees/zod/pr-<N>\nnub install --frozen-lockfile      # fast, Nub's store is shared\n```\n\nDo **not** use `gh pr checkout --detach` — it detaches your current working tree instead of creating a worktree.\n\nThen, in the worktree:\n\n1. Read the diff in full context, not just the hunks. What did it touch that the author did not think about?\n2. `git log --oneline main..HEAD` — is this rebased on anything current, or written against a version of the code that has since moved?\n3. Run the tests the change implicates: `nub exec --node vitest run <path>`. Run the full suite only when the change is broad.\n4. Judge test coverage against the repo's own bar: a feature or bug fix without a test is incomplete, and tests must be TypeScript.\n5. Probe the edge cases the author's tests skip — write them into `$ZOD_ROOT/.triage/prs/<N>/` and run them. A passing suite over a shallow test is not evidence.\n6. Write `$ZOD_ROOT/.triage/prs/<N>/results.md`, then report back per `## Reporting back`.\n\n**Clean up when done**, or worktrees accumulate across a large sweep:\n\n```bash\ncd \"$ZOD_ROOT\"\ngit worktree remove ~/.cursor/worktrees/zod/pr-<N> && git branch -D pr-<N>\n```\n\nKeep the worktree only when the verdict is `merge-with-changes` and you have been asked to push the changes; AGENTS.md covers pushing back to a contributor's fork, including preserving their commits.\n\n## Sweeping in bulk\n\nSeed the queue, then work it:\n\n```bash\ngh pr list -R colinhacks/zod --state open --limit 400 --json number,title,updatedAt\n```\n\nResume by asking disk, not memory:\n\n```bash\ngrep -l 'status: done' .triage/prs/*/results.md | wc -l                    # progress\ngrep -h '^verdict:' .triage/prs/*/results.md | sort | uniq -c              # verdict spread\nnub --node .claude/skills/triage/scripts/reindex.mjs prs             # regenerate .triage/prs/index.md\n```\n\n`reindex.mjs` rebuilds an index table from frontmatter, so the index is derived and never hand-edited for PRs. Pass `issues`, `prs`, or `advisories`.\n\n`nub run dev scripts/triage-signal.ts` regenerates `.triage/signal-report.md`, which ranks the whole open backlog by engagement, flags security content, and groups likely-duplicate clusters. Start a sweep there rather than at the top of `gh pr list` — roughly a third of the open backlog is duplicates, and the clusters collapse many tickets into one decision.\n\nWhen parallelizing across sub-agents, give each agent a disjoint set of numbers and have each write only its own ticket directories. Never have two agents share a results file.\n","AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Development Commands\n\nUse Nub exclusively for dependency management, scripts, and TypeScript execution. The root `package.json` declares the workspaces and pins Nub; `.nvmrc` selects Node. Nub reads and writes the existing `pnpm-lock.yaml` without requiring pnpm.\n\nRun scripts with `nub run`, repository TypeScript with `nub <file>`, and installed tools with `nub exec --node <tool>`. Keep third-party tools and built-package compatibility probes on plain Node; Nub's augmentation must not mask resolution errors or change test behavior. Use `nub install --frozen-lockfile` for reproducible installs and `nub add` / `nub remove` to manage dependencies.\n\nKey commands:\n\n- `nub run build` - Build all packages (runs recursive build command)\n- `nub exec --node vitest run` - Run all tests with Vitest. Includes the compile-mode project, which re-runs the zod tests with global AOT compilation enabled (see `wiki/compile.md`).\n- `nub exec --node vitest run <path>` - Run specific test file (e.g., `packages/zod/src/v4/classic/tests/string.test.ts`)\n- `nub exec --node vitest run <path> -t \"<pattern>\"` - Run specific test(s) within a file (e.g., `-t \"MAC\"`)\n- `nub exec --node vitest run --update` - Update all test snapshots\n- `nub exec --node vitest run <path> --update` - Update snapshots for specific test file\n- `nub run test:watch` - Run tests in watch mode\n- `nub exec --node vitest run --coverage` - Run tests with coverage report\n- `nub run test:compile` - Focused alias for just the compile-mode project. Already covered by `nub run test`; use this when iterating on compile-related changes.\n- `nub run dev` - Execute TypeScript with Nub under source conditions\n- `nub run dev <file>` - Execute `<file>` with Nub & proper resolution conditions. Usually use for `play.ts`.\n- `nub run dev:play` - Quick alias to run play.ts for experimentation\n- `nub run check:comments` - Fail on stacked `//` comment lines (`--fix` joins them)\n- `nub run lint` - Run biome linter with auto-fix\n- `nub run format` - Format code with biome\n- `nub run fix` - Run both format and lint\n\n## Rules\n\n- Nub v0.8.3 and Node.js v24+ required; Nub provisions Node from `.nvmrc`\n- ES modules are used throughout (`\"type\": \"module\"`)\n- All tests must be written in TypeScript - never use JavaScript\n- Use `play.ts` for quick experimentation; use proper tests for all permanent test cases\n- Features without tests are incomplete - every new feature or bug fix needs test coverage\n- Don't skip tests due to type issues - fix the types instead\n- Test both success and failure cases with edge cases\n- Keep added tests as minimal and dense as possible without sacrificing comprehensiveness; avoid redundant assertions or broad fixtures when a focused case proves the behavior.\n- No log statements (`console.log`, `debugger`) in tests or production code\n- Never stack prose across consecutive `//` lines. Lines have no maximum width here — the editor wraps for display — so a paragraph split across several `//` lines is just a hard-wrapped line, and hard wrapping breaks search, diffs and editing. Write one long `//` instead. `nub run check:comments` enforces this in pre-commit and CI; `--fix` joins the offenders. Commented-out code, `@ts-`/`@__NO_SIDE_EFFECTS__`-style pragmas, bullet lists, and blocks separated by a bare `//` are exempt. When two adjacent comments describe two different statements, separate them with a blank line rather than joining them.\n- Keep comments SHORT AND TIGHT — one lowercase sentence fragment, one clause, no trailing period. Never a capitalized full sentence, and never two of them. Extreme concision: say only what the code cannot say, and cut the setup sentence, the recap, and the same point restated in different words; a comment that needs three sentences usually means the code should be clearer. Identifiers keep their real casing (`Error`, `parse()`), only the prose is lowercase.\n- Ask before generating new files\n- Use `util.defineLazy()` for computed properties to avoid circular dependencies\n- Never branch on specific schema types in shared code. No `def.type === \"optional\"` conditionals, no hardcoded lists of wrapper type names, no walks up the wrapper chain hunting for a particular type. Every schema type added later silently falls through such a check, and the list is wrong the moment someone writes a new wrapper. When a shared path needs to know something about a schema, express it as a structural property on the internals — `optin`/`optout`, `values`, `pattern`, `propValues` — and let each type declare its own answer. This is not negotiable in the parse paths; a PR that adds edge-case conditional logic keyed on schema types will be rejected regardless of how well it is tested.\n- Performance is critical - parameter reassignment is allowed for optimization\n- Any change to `packages/zod/src` must be weighed on **all three axes: runtime performance, memory consumption, and bundle size** — see \"The three axes\" below. A change that improves one and is only checked on that one is not finished.\n- ALWAYS use the `gh` CLI to fetch GitHub information (issues, PRs, etc.) instead of relying on web search or assumptions\n- Keep JSDoc as minimal as possible. A self-explanatory type or symbol name needs no doc comment. When a comment is genuinely required, write one short sentence describing behavior — not history, rationale, or examples. Don't add interface-level JSDoc that just restates the interface name.\n- When you've modified a PR (or opened/closed/commented on one), include the PR URL liberally in summary messages — at minimum once at the end of any reply that touched it\n- When creating a PR, do not include a separate test plan section in the body. Link to any relevant issues under discussion, and use the same copywriting guidelines from \"Commenting on issues and PRs\": concise maintainer voice, prose over templates, and validation details only when they are material to the reader.\n- Format validators (`z.iso.*`, `z.email()`, `z.url()`, `z.uuid()`, …) are deliberately narrower than the specs they're named after. \"The spec allows X\" is not a reason to accept X — see \"Format validators: spec compliance is not the bar\" below.\n- NEVER bump the version in `packages/zod/package.json` (or any package's `package.json`), and NEVER run `gh workflow run release.yml`. Publishing is a maintainer-only dispatch behind an approval. If a version bump is genuinely needed, ask first.\n\n## The three axes\n\nZod is judged on **runtime performance**, **memory consumption**, and **bundle size** at once, and they trade against each other constantly. Optimizing one in isolation is how regressions land: a change that buys bundle bytes can cost construction speed, and one that saves memory can cost both. Any non-trivial change to `packages/zod/src` — and any change to `core/` at all, since every build ships it — needs a number on all three before it is done. Report them together, including the ones that got worse.\n\n`zod/mini` deserves its own line on the bundle axis: it is sold on size, so a fixed cost lands very differently there (~200 B is 6% of the smallest mini bundle and 1% of classic's).\n\n| axis    | how to measure                                                                                                                                    |\n| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| runtime | `packages/bench/*.ts` via `nub run bench <name>`, plus construction cost — parse and construction move independently                                 |\n| memory  | `packages/bench/memory/schema-footprint.ts` and `realworld.ts` (retained bytes per schema; run under `--expose-gc`)                               |\n| bundle  | bundle a `packages/treeshake` fixture with esbuild `--minify` under `--conditions=@zod/source` and gzip it, for both a classic and a mini fixture |\n\n### Benchmarking traps\n\nThese produced hours of wrong conclusions; check them before believing a number.\n\n- **Check `uptime` first.** A loaded machine invents effects of ±16%. Numbers taken above a load average of ~8 are worthless.\n- **Interleave the two revisions.** Running all of A then all of B puts any drift entirely on whichever ran second. Alternating round-by-round took per-side spread from 2.5x to 4%.\n- **Loading two zod revisions into one process makes call sites polymorphic.** It is fine for comparing construction, unreliable for tight leaf parses.\n- **Allocating benchmarks can't be timed by a fixed-duration loop.** `z.array().parse()` allocates per call, so a time-boxed harness samples whatever the collector is doing — it gave +8.2% and −17.3% on consecutive identical runs. Use a fixed iteration count with `gc()` between samples and take the minimum.\n- **Make sure the work isn't optimized away.** If a micro-benchmark reports ~1e9 ops/sec, V8 eliminated the loop; consume the result.\n\n### Property placement, specifically\n\nMost of the memory in a schema is its own-property count, not its closures. V8 sizes an instance's backing store in steps and `$constructor` assigns nothing itself, so instances get no in-object slots: **≤12 own properties cost 128 bytes, 13–20 cost 848, ≥21 cost 1616** (`packages/bench/memory/prop-slack.ts` measures this). Members therefore live on the prototype and materialize per instance on first read.\n\nIf you touch that machinery, three things bite:\n\n- **Moving a property between definition sites changes its descriptor**, and writability/enumerability/configurability are part of the public contract. `packages/bench/memory/` has a surface-diff approach for this: dump every descriptor, alias and assign/delete semantic on both revisions and diff them. It caught four real regressions that tests did not.\n- **Redefining an accessor demotes the object to dictionary mode**, which cost 2x on `z.object().parse` once. Run `packages/bench/memory/dict-mode.ts` after any change here — every instance must report `fast`.\n- **A bound function pays a call-time trampoline.** Fine for cold builder methods, measurably not fine on the parse path.\n\n## Format validators: spec compliance is not the bar\n\nZod's format validators — `z.iso.*`, `z.email()`, `z.url()`, `z.uuid()`, and the rest — are named after specs but do not implement them. Each one matches the profile that real producers emit and real consumers accept, which is almost always far narrower than what the grammar permits. That narrowness is the product, not a gap in it.\n\nSo **\"the spec allows X, therefore Zod must accept X\" is not an argument**, and a PR that widens a format regex on that basis alone will be closed. The question is never what the spec permits; it's whether accepting the wider form helps more people than rejecting it hurts. A form that shows up in real payloads and breaks real integrations is worth fixing. A form that only appears when someone reads the grammar and constructs a string from it is not — when one of those reaches a validator in production it's a bug or an attack, and rejecting it is the useful behavior.\n\nThree things make a widening request an automatic no:\n\n- **The spec permits the form only \"by mutual agreement\" of the two parties.** A general-purpose validator is never party to that agreement, so it can't honor the clause. ISO 8601 expanded years (`+010000-01-01T00:00:00.000Z`) are the canonical case: the standard also requires the parties to agree the digit count, and runtimes disagree — JavaScript emits six digits, Java's `Instant` emits five, Python rejects both. Declined in #6154.\n- **The wider form breaks the JSON Schema output.** `z.iso.datetime()` and `z.iso.date()` emit `format: \"date-time\"` and `format: \"date\"`, which mean RFC 3339 — four-digit years, no sign. Widening those regexes makes `z.toJSONSchema()` emit a pattern that conformant consumers reject.\n- **The cost lands on everyone.** These regexes live in `core/regexes.ts`, so every added alternation is bytes in every bundle including `zod/mini`, plus more backtracking on a hot parse path. Weigh it on all three axes above before you argue for it at all.\n\nFor calibration on how narrow these already are: `z.iso.datetime()` rejects basic format (`20200101T061500Z`), week dates (`2020-W01-1`), ordinal dates (`2020-001`), comma decimal separators, and `24:00`. Every one of those is valid ISO 8601. None of them are going in either.\n\nPoint anyone who genuinely needs a wider format at `z.string().regex()` or `z.string().refine()`. That escape hatch is the answer, not a wider default.\n\n## Triaging issues and PRs\n\nFollow the `triage` skill at [`.claude/skills/triage/SKILL.md`](.claude/skills/triage/SKILL.md) whenever you're asked to investigate, triage, or form an opinion on an issue or PR. It is the single source of truth for the procedure. Claude and Codex both auto-discover it (Codex via the `.codex/skills` symlink); read it directly if your agent doesn't.\n\nFor a draft **security advisory** — a GHSA id, the Security tab, a private vulnerability report — use the `security-advisory` skill at [`.claude/skills/security-advisory/SKILL.md`](.claude/skills/security-advisory/SKILL.md) instead. It shares the conventions above but reorders the work: the fix lands on `main` before any reporter comment is drafted, since the comment's whole value is the PR it links to.\n\nWrite-ups live in the gitignored `.triage/` tree — one directory per ticket, `.triage/issues/NNNN/results.md` and `.triage/prs/NNNN/results.md`, with scratch files and repros alongside. When investigating a PR from its worktree, resolve the root repo first (`git worktree list | head -1`) and write results back there; a relative write lands in the worktree's own ignored `.triage/` where nobody will find it.\n\n## Iterating on a contributor PR in a worktree\n\nWhen asked to make changes on top of an open PR (e.g. as a maintainer review suggestion), use a worktree so `main` stays clean:\n\n```bash\n# 1. Fetch the PR as a local branch and create a worktree for it\ngit fetch origin pull/<N>/head:pr-<N>\ngit worktree add ~/.cursor/worktrees/zod/pr-<N> pr-<N>\ncd ~/.cursor/worktrees/zod/pr-<N>\nnub install --frozen-lockfile   # fast, Nub's store is shared across worktrees\n\n# 2. Look up the PR's head info — you'll need the contributor's fork URL\n#    and the head ref name to push back.\ngh pr view <N> --repo colinhacks/zod \\\n  --json headRefName,headRepositoryOwner,maintainerCanModify\n\n# 3. If maintainerCanModify is true, add the fork as a remote and push to\n#    the PR's head ref (NOT to your local branch name).\ngit remote add <contributor> git@github.com:<contributor>/zod.git\ngit push <contributor> pr-<N>:<headRefName>                 # first push\ngit push <contributor> pr-<N>:<headRefName> --force-with-lease   # for amends\n```\n\nNotes:\n\n- Do NOT use `gh pr checkout --detach` for this — it moves your _current_ working tree into detached HEAD instead of creating a worktree.\n- Husky pre-commit runs biome format/lint via lint-staged; pre-push runs the full vitest suite. Both are fast and act as a safety net — don't bypass with `--no-verify` unless you have a specific reason.\n- **Preserve contributor commits.** Never `git reset --hard` or otherwise rewrite history that erases the contributor's work, even if you're rewriting the actual change. They need to stay in the PR's commit list to get credit on the merged PR. If their approach was wrong, add a `Revert \"...\"` commit (or just a plain commit that undoes those lines) and then add your replacement commit on top. Force-pushing a single clobbering commit strips them from the GitHub contributors graph.\n- When done, clean up: `git worktree remove ~/.cursor/worktrees/zod/pr-<N>` and `git branch -D pr-<N>` (and optionally `git remote remove <contributor>`).\n\n## Commenting on issues and PRs\n\nWhen posting on a maintainer's behalf via `gh` (PR comments, issue comments, reviews), match the house tone. Load the `prose-writing` skill before you write any of it — it is the general copy guide, and this section layers on top of it. The register is authoritative and friendly — concise, not bubbly, not over-explaining, not effusive. Comments come from a maintainer handing down decisions, not negotiating them. Friendly does not mean deferential.\n\n- Exclamation points are fine in moderation, especially to soften a decline or close out a thread (\"Thanks for looking into this!\"). Don't stack them and don't sprinkle them through technical writeups.\n- Skip effusive praise: \"Great work\", \"Awesome\", \"Thanks so much for this\", \"Thanks for the careful writeup\", \"you clearly read the RFC\". A short flat-affect affirmation walks the line well — \"Good investigation.\" or \"Solid catch.\" with a period, no exclamation, no superlatives. Warmth otherwise comes from a short closer (\"Thanks for looking into this, though 👍\"), not a preamble that butters up the contributor before the decision.\n- No \"PTAL\", \"WDYT\", or sign-off flourishes asking the contributor to re-review changes the maintainer pushed on top. State what changed and the merge intent. (\"LGTM\" is fine as a literal verdict at the end of a substantive review, not as a sign-off.)\n- When the user gives you exact wording for a comment, use it verbatim (fixing only obvious typos). Do not \"improve\" their phrasing to match this style guide — their direct instruction wins.\n- Lead with the decision or action: \"Going to merge as-is.\" \"Closing this out.\" \"I'd be open to a top-level utility but not as a method.\" Then the reasoning.\n- First person, owned opinions. \"I don't think this should be a method.\" \"I'm hesitant to add this.\" Don't hide behind passive voice or \"we could perhaps consider\".\n- Speak with authority. No hedging (\"maybe\", \"I think perhaps\", \"if that's okay\"), no apologizing for decisions, no asking permission to land changes the maintainer has already made. Decisions are stated as decisions.\n- Be direct when declining, but not curt. \"out of scope\", \"behaving as intended\", \"this is more complicated than it looks\" — firm, with a concrete reason. A friendly closer (\"thanks for looking into this\") is fine.\n- Cross-reference by number: `#4433`, `commit 2f8414bc`, `merged in #5718`. Concrete and verifiable.\n- Length matches substance, and substance is almost never as much as you think. Two or three sentences closes most threads. How long you spent investigating has nothing to do with how long the reply should be — a week of benchmarking still closes in three sentences. The `triage` skill works this through against a real decline.\n- No file paths, line numbers, or symbol names unless the reader has to open that file. They turn a two-line decision into a report, and nobody asked you to show your work. Say what the code does instead — \"the parse context is per-call state now\", not \"`core/memoizer.ts:133` writes onto the context\". The detail belongs in the `.triage/` write-up.\n- Pick the one or two strongest reasons and write them as prose. Resist enumerating every objection in a bullet list — even when each point is fair, it reads as piling on. The strongest argument plus a concrete escape hatch (e.g. \"`z.email().max(254)` already does this\") is usually enough.\n- Don't lift informal or coarse phrasing from external sources (blog posts, issues, comments) into the maintainer voice, even in quotes. Paraphrase the substance — quoted-in-context still reads as the maintainer talking.\n- Use prose with inline backticks for symbols. Reach for fenced code blocks only when showing non-trivial code is genuinely clearer than describing it.\n- Skip emojis in substantive technical writeups. A small `👍` in a casual closer is good — it keeps a decline or sign-off sounding warm and friendly without leaning on praise.\n- Bot mentions are bare imperatives: `@pullfrog review`, `@pullfrog fix merge conflicts`, `@pullfrog re-review fresh.`\n- When pushing a follow-up on top of a contributor's PR, state what changed, why it differs from the original approach, and that the maintainer is merging. Never ask the contributor to review the maintainer's changes — they are final, not a proposal. Don't thank them for \"letting\" the maintainer rewrite their work.\n- When posting comments with code samples via `gh`, do NOT pass the body inline through a heredoc that requires escaping backticks. Backslash-escaped backticks (`` \\` ``) inside a `$(cat <<'EOF' ... EOF)` body get sent to GitHub literally and break inline code and template literals inside fenced blocks. Instead, write the comment to a file and pass it via `--body-file <path>` (for `gh pr/issue comment`) or `-F body=@<path>` (for `gh api`). This preserves backticks and `${...}` exactly as written.\n\n## Pushing to a new branch (don't accidentally push to main)\n\nWhen the user asks for a \"new PR\" or \"new branch\", the work has to land on a non-`main` ref on the remote. The footgun: `git worktree add <path> -b <branch> origin/main` (and `git checkout -b <branch> origin/main`) silently set the new branch's upstream to `refs/heads/main` because the start point is a remote-tracking ref. A subsequent `git push -u origin <branch>` then pushes to `refs/heads/main`, not to a new remote branch. Two of these in a row have happened.\n\nAvoid by being explicit on the first push:\n\n```bash\n# Always use a refspec on the initial push so the remote ref name is unambiguous.\ngit push -u origin <branch>:refs/heads/<branch>\n```\n\nThen verify the output. The line you want to see is:\n\n```\n* [new branch]      <branch> -> <branch>\n```\n\nIf the right-hand side says `main`, the push went to main — abort or revert.\n\nUse `git push origin HEAD:refs/heads/<branch>` for subsequent pushes if you didn't `-u` originally. Don't rely on `git push -u origin <branch>` alone — its behavior depends on the upstream config, which `worktree add -b ... origin/main` set wrong for you.\n","CLAUDE.md":"AGENTS.md","packages/bench/typia-case/README.md":"# typia case\n\ntypia generates its validators with a TypeScript transformer, so this directory holds a prebuilt artifact rather than a source import. `build/index.cjs` is generated from `src/index.ts`.\n\nThe transformer does not run inside this workspace: `tspc` live mode crashes on typia's file transformer, and the alternative — `ts-patch install` — patches the TypeScript that every package here shares. The artifact is therefore built in a throwaway project and copied in. The generated file inlines every check, so it depends only on the `typia` runtime already in this package.\n\nTo regenerate:\n\n```sh\nmkdir -p /tmp/typia-build/src && cd /tmp/typia-build\nnub init -y --js --no-install --no-git\nnub add typia@9.7.2 typescript@5.9.3 ts-patch@3.3.0\ncp <repo>/packages/bench/typia-case/src/index.ts src/index.ts\ncp <repo>/packages/bench/typia-case/tsconfig.json tsconfig.json\nnub exec --node ts-patch install && nub exec --node tsc -p tsconfig.json\ncp build/index.js <repo>/packages/bench/typia-case/build/index.cjs\n```\n","packages/docs/.vscode/settings.json":"{\n  \"editor.defaultFormatter\": \"biomejs.biome\",\n  \"editor.formatOnSave\": true,\n  \"editor.codeActionsOnSave\": {\n    \"source.organizeImports.biome\": \"explicit\",\n    \"source.fixAll\": \"always\",\n    \"quickfix.biome\": \"explicit\"\n  },\n  \"editor.wordWrap\": \"wordWrapColumn\",\n  \"editor.wordWrapColumn\": 120,\n  \"[mdx]\": {\n    \"editor.defaultFormatter\": \"unifiedjs.vscode-mdx\",\n    \"editor.formatOnSave\": true\n  }\n}\n","packages/docs/README.md":"# onepager-fuma\n\n<Tabs groupId=\"lib\" items={[\"Zod\", \"Zod Mini\"]}>\n<Tab value=\"Zod\">\n</Tab>\n<Tab value=\"Zod Mini\">\n</Tab>\n</Tabs>\n","packages/docs/content/meta.json":"{\n  \"root\": true,\n  \"pages\": [\n    \"---Documentation---\",\n    \"index\",\n    \"basics\",\n    \"api\",\n    \"error-customization\",\n    \"error-formatting\",\n    \"metadata\",\n    \"json-schema\",\n    \"codecs\",\n    \"compile\",\n    \"ecosystem\",\n    \"library-authors\",\n    \"---Packages---\",\n    \"packages/zod\",\n    \"packages/mini\",\n    \"packages/core\"\n  ]\n}\n","packages/docs/package.json":"{\n  \"name\": \"@zod/docs\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"nub exec --node next build --turbopack\",\n    \"dev\": \"nub exec --node next dev\",\n    \"start\": \"nub exec --node next start\",\n    \"postinstall\": \"nub exec --node fumadocs-mdx\"\n  },\n  \"dependencies\": {\n    \"@inkeep/cxkit-react\": \"^0.5.117\",\n    \"@radix-ui/react-accordion\": \"^1.2.4\",\n    \"@vercel/analytics\": \"^1.5.0\",\n    \"@vercel/og\": \"^0.6.8\",\n    \"fumadocs-core\": \"15.1.0\",\n    \"fumadocs-mdx\": \"11.5.6\",\n    \"fumadocs-ui\": \"15.1.0\",\n    \"gray-matter\": \"^4.0.3\",\n    \"lucide-react\": \"^0.483.0\",\n    \"next\": \"15.5.25\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-tweet\": \"^3.3.1\",\n    \"remark\": \"^15.0.1\",\n    \"remark-gfm\": \"^4.0.1\",\n    \"remark-mdx\": \"^3.1.0\",\n    \"remark-stringify\": \"^11.0.0\",\n    \"shiki\": \"^3.8.1\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"^4.2.4\",\n    \"@types/mdx\": \"^2.0.13\",\n    \"@types/node\": \"22.13.10\",\n    \"@types/react\": \"^19.0.10\",\n    \"@types/react-dom\": \"^19.0.4\",\n    \"postcss\": \"^8.5.12\",\n    \"tailwindcss\": \"^4.2.4\",\n    \"typescript\": \"^5.8.2\"\n  }\n}\n","packages/docs/tsconfig.json":"{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"paths\": {\n      \"@/.source\": [\"./.source/index.ts\"],\n      \"@/*\": [\"./*\"]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"types\": [\"mdx\", \"node\", \"react\", \"react-dom\", \"@vercel/og\"]\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\", \"components/if.tsx\", \"types/**/*.d.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n","packages/docs/tsconfig.test.json":"{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"types\": [\"node\", \"vitest\"]\n  },\n  \"include\": [\"**/*.test.ts\"]\n}\n","packages/docs/vercel.json":"{\n  \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n  \"installCommand\": \"bash -o pipefail -c 'curl -fsSL https://raw.githubusercontent.com/nubjs/nub/0ae8783f1f93763c56dfc827892cfb679e8a0a77/install.sh | bash -s -- 0.8.3 && export PATH=\\\"$HOME/.nub/bin:$PATH\\\" && cd ../.. && nub install --frozen-lockfile'\",\n  \"buildCommand\": \"export PATH=\\\"$HOME/.nub/bin:$PATH\\\" && nub run build\"\n}\n","packages/mini/README.md":"# @zod/mini\n\nZod Mini as a standalone package. It re-exports [`zod/mini`](https://zod.dev/packages/mini) from a peer dependency on `zod`, so every schema it creates is a `zod` schema.\n\n```sh\nnpm install zod @zod/mini\n```\n\n```ts\nimport * as z from \"@zod/mini\";\n\nconst User = z.object({\n  name: z.string(),\n  age: z.number().check(z.int(), z.positive()),\n});\n\nz.parse(User, { name: \"Colin\", age: 30 });\n```\n\nThe version tracks `zod`: each `@zod/mini@4.x.y` requires `zod@^4.x.0`.\n","packages/resolution/README.md":"# Package resolution checks\n\nRun from the repository root:\n\n```sh\nnub run build\nnub run --filter @zod/resolution test:all\n```\n\nThese checks inspect the published declarations with Are the Types Wrong and execute the built CommonJS and ES modules on plain Node through Nub.\n","packages/tsc/README.md":"# Zod perftesting\n\nRun `nub run generate` to generate random Zod schemas.\n\nRun `nub run build:bench` to run `tsc` with `extendedDiagnostics`.\n\nThe workspace dependency points to the local Zod package. Rebuild Zod after changing its typings.\n","packages/zod/README.md":"<p align=\"center\">\n  <img src=\"logo.svg\" width=\"200px\" align=\"center\" alt=\"Zod logo\" />\n  <h1 align=\"center\">Zod</h1>\n  <p align=\"center\">\n    TypeScript-first schema validation with static type inference\n    <br/>\n    by <a href=\"https://x.com/colinhacks\">@colinhacks</a>\n  </p>\n</p>\n<br/>\n\n<p align=\"center\">\n<a href=\"https://github.com/colinhacks/zod/actions?query=branch%3Amain\"><img src=\"https://github.com/colinhacks/zod/actions/workflows/test.yml/badge.svg?event=push&branch=main\" alt=\"Zod CI status\" /></a>\n<a href=\"https://opensource.org/licenses/MIT\" rel=\"nofollow\"><img src=\"https://img.shields.io/github/license/colinhacks/zod\" alt=\"License\"></a>\n<a href=\"https://www.npmjs.com/package/zod\" rel=\"nofollow\"><img src=\"https://img.shields.io/npm/dw/zod.svg\" alt=\"npm\"></a>\n<a href=\"https://discord.gg/KaSRdyX2vc\" rel=\"nofollow\"><img src=\"https://img.shields.io/discord/893487829802418277?label=Discord&logo=discord&logoColor=white\" alt=\"discord server\"></a>\n<a href=\"https://github.com/colinhacks/zod\" rel=\"nofollow\"><img src=\"https://img.shields.io/github/stars/colinhacks/zod\" alt=\"stars\"></a>\n</p>\n\n<div align=\"center\">\n  <a href=\"https://zod.dev/api\">Docs</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://discord.gg/RcG33DQJdf\">Discord</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://twitter.com/colinhacks\">𝕏</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://bsky.app/profile/zod.dev\">Bluesky</a>\n  <br />\n</div>\n\n<br/>\n<br/>\n\n### [Read the docs →](https://zod.dev/api)\n\n<br/>\n<br/>\n\n## What is Zod?\n\nZod is a TypeScript-first validation library. Define a schema and parse some data with it. You'll get back a strongly typed, validated result.\n\n```ts\nimport * as z from \"zod\";\n\nconst User = z.object({\n  name: z.string(),\n});\n\n// some untrusted data...\nconst input = {\n  /* stuff */\n};\n\n// the parsed result is validated and type safe!\nconst data = User.parse(input);\n\n// so you can use it with confidence :)\nconsole.log(data.name);\n```\n\n<br/>\n\n## Features\n\n- Zero external dependencies\n- Works in Node.js and all modern browsers\n- Tiny: `2kb` core bundle (gzipped)\n- Immutable API: methods return a new instance\n- Concise interface\n- Works with TypeScript and plain JS\n- Built-in JSON Schema conversion\n- Extensive ecosystem\n\n<br/>\n\n## Installation\n\n```sh\nnpm install zod\n```\n\n<br/>\n\n## Basic usage\n\nBefore you can do anything else, you need to define a schema. For the purposes of this guide, we'll use a simple object schema.\n\n```ts\nimport * as z from \"zod\";\n\nconst Player = z.object({\n  username: z.string(),\n  xp: z.number(),\n});\n```\n\n### Parsing data\n\nGiven any Zod schema, use `.parse` to validate an input. If it's valid, Zod returns a strongly-typed _deep clone_ of the input.\n\n```ts\nPlayer.parse({ username: \"billie\", xp: 100 });\n// => returns { username: \"billie\", xp: 100 }\n```\n\n**Note** — If your schema uses certain asynchronous APIs like `async` [refinements](https://zod.dev/api#refinements) or [transforms](https://zod.dev/api#transforms), you'll need to use the `.parseAsync()` method instead.\n\n```ts\nconst schema = z.string().refine(async (val) => val.length <= 8);\n\nawait schema.parseAsync(\"hello\");\n// => \"hello\"\n```\n\n### AOT compilation\n\nFor hot validation paths, `z.compile(schema)` returns a schema clone with an ahead-of-time compiled fast path. Valid inputs take the compiled path; invalid inputs fall back to the regular parser so error reporting stays identical.\n\nAcross a 55-schema benchmark the median speedup is **2.4x**, and it scales with how much work the schema does per parse: a large array of objects is ~9x, a 20-key object ~9x, a nested object ~4.5x, while a bare `z.string()` gains nothing — compilation removes per-node dispatch and allocation, and a single `typeof` has none to remove.\n\n```ts\nconst CompiledPlayer = z.compile(Player);\n\nCompiledPlayer.parse({ username: \"billie\", xp: 100 });\n```\n\nTo enable compilation globally for schemas constructed after import:\n\n```ts\nimport \"zod/compile\"; // place before modules that define schemas\n```\n\nThings to know:\n\n- Compilation uses `new Function`. Global mode is automatically disabled when `z.config({ jitless: true })` is set (e.g. CSP environments); calling `z.compile()` directly is an explicit opt-in.\n- Schemas with async refinements or transforms can't be compiled, and neither can a few other constructs. That is not an error: `z.compile()` hands the schema back unchanged and it keeps using the regular parser, exactly as global mode leaves it. Pass `{ strict: true }` to throw `ZodCompileAsyncError` / `ZodCompileUnsupportedError` instead.\n- On invalid input, refinements and transforms may run twice (fast path, then fallback).\n- Deriving a new schema from a compiled one (`.refine()`, `.extend()`, etc.) returns an uncompiled schema — compile the final schema.\n\nSee [`compile` docs](https://zod.dev/compile) for details.\n\n### Handling errors\n\nWhen validation fails, the `.parse()` method will throw a `ZodError` instance with granular information about the validation issues.\n\n```ts\ntry {\n  Player.parse({ username: 42, xp: \"100\" });\n} catch (err) {\n  if (err instanceof z.ZodError) {\n    err.issues;\n    /* [\n      {\n        expected: 'string',\n        code: 'invalid_type',\n        path: [ 'username' ],\n        message: 'Invalid input: expected string, received number'\n      },\n      {\n        expected: 'number',\n        code: 'invalid_type',\n        path: [ 'xp' ],\n        message: 'Invalid input: expected number, received string'\n      }\n    ] */\n  }\n}\n```\n\nTo avoid a `try/catch` block, you can use the `.safeParse()` method to get back a plain result object containing either the successfully parsed data or a `ZodError`. The result type is a [discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions), so you can handle both cases conveniently.\n\n```ts\nconst result = Player.safeParse({ username: 42, xp: \"100\" });\nif (!result.success) {\n  result.error; // ZodError instance\n} else {\n  result.data; // { username: string; xp: number }\n}\n```\n\n**Note** — If your schema uses certain asynchronous APIs like `async` [refinements](https://zod.dev/api#refinements) or [transforms](https://zod.dev/api#transforms), you'll need to use the `.safeParseAsync()` method instead.\n\n```ts\nconst schema = z.string().refine(async (val) => val.length <= 8);\n\nawait schema.safeParseAsync(\"hello\");\n// => { success: true; data: \"hello\" }\n```\n\n### Inferring types\n\nZod infers a static type from your schema definitions. You can extract this type with the `z.infer<>` utility and use it however you like.\n\n```ts\nconst Player = z.object({\n  username: z.string(),\n  xp: z.number(),\n});\n\n// extract the inferred type\ntype Player = z.infer<typeof Player>;\n\n// use it in your code\nconst player: Player = { username: \"billie\", xp: 100 };\n```\n\nIn some cases, the input & output types of a schema can diverge. For instance, the `.transform()` API can convert the input from one type to another. In these cases, you can extract the input and output types independently:\n\n```ts\nconst mySchema = z.string().transform((val) => val.length);\n\ntype MySchemaIn = z.input<typeof mySchema>;\n// => string\n\ntype MySchemaOut = z.output<typeof mySchema>; // equivalent to z.infer<typeof mySchema>\n// number\n```\n",".cursorrules (typescript-zod-tailwind-nextjs-cursorrules-prompt-.mdc)":"---\ndescription: \"Cursor rules for TypeScript development with Zod, Tailwind, and Next.js integration.\"\nglobs: **/*\nalwaysApply: false\n---\n# Coding Style Guide\n\nCode Style and Structure:\n- Write concise, technical TypeScript code with accurate examples\n- Use functional and declarative programming patterns; avoid classes\n- Prefer iteration and modularization over code duplication\n- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError)\n- Structure files: exported component, subcomponents, helpers, static content, types\n\nNaming Conventions:\n- Use lowercase with dashes for directories (e.g., components/auth-wizard)\n- Favor named exports for components\n\nTypeScript Usage:\n- Use TypeScript for all code; prefer interfaces over types\n- Avoid enums; use maps instead\n- Use functional components with TypeScript interfaces\n- Use Zod for form validation\n\nSyntax and Formatting:\n- Use the \"function\" keyword for pure functions\n- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements\n- Use declarative JSX\n\nError Handling and Validation:\n- Prioritize error handling: handle errors and edge cases early\n- Use early returns and guard clauses\n- Implement proper error logging and user-friendly messages\n- Use Zod for form validation\n- Model expected errors as return values in Server Actions\n- Use error boundaries for unexpected errors\n\nUI and Styling:\n- Use Shadcn UI, Radix, and Tailwind Aria for components and styling\n- Implement responsive design with Tailwind CSS; use a desktop-first approach\n\nPerformance Optimization:\n- Minimize 'useEffect', and 'setState'; favor React Remix Components (RSC)\n- Wrap client components in Suspense with fallback\n- Use dynamic loading for non-critical components\n- Optimize images: use WebP format, include size data, implement lazy loading\n\nKey Conventions:\n- Use proper URL search parameter state management\n- Optimize Web Vitals (LCP, CLS, FID)\n- Limit 'use client'\n\nWhen React Server Components (RSC) are used:\n- Favor server components and Next.js SSR\n- Use only for Web API access in small components\n- Avoid for data fetching or state management\n\nFollow React Remix docs for Data Fetching, Rendering, and Routing\n\nFollow Next.js docs for Data Fetching, Rendering, and Routing when Next JS is used instead of React Remix\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Development Commands\n\nUse Nub exclusively for dependency management, scripts, and TypeScript execution. The root `package.json` declares the workspaces and pins Nub; `.nvmrc` selects Node. Nub reads and writes the existing `pnpm-lock.yaml` without requiring pnpm.\n\nRun scripts with `nub run`, repository TypeScript with `nub <file>`, and installed tools with `nub exec --node <tool>`. Keep third-party tools and built-package compatibility probes on plain Node; Nub's augmentation must not mask resolution errors or change test behavior. Use `nub install --frozen-lockfile` for reproducible installs and `nub add` / `nub remove` to manage dependencies.\n\nKey commands:\n\n- `nub run build` - Build all packages (runs recursive build command)\n- `nub exec --node vitest run` - Run all tests with Vitest. Includes the compile-mode project, which re-runs the zod tests with global AOT compilation enabled (see `wiki/compile.md`).\n- `nub exec --node vitest run <path>` - Run specific test file (e.g., `packages/zod/src/v4/classic/tests/string.test.ts`)\n- `nub exec --node vitest run <path> -t \"<pattern>\"` - Run specific test(s) within a file (e.g., `-t \"MAC\"`)\n- `nub exec --node vitest run --update` - Update all test snapshots\n- `nub exec --node vitest run <path> --update` - Update snapshots for specific test file\n- `nub run test:watch` - Run tests in watch mode\n- `nub exec --node vitest run --coverage` - Run tests with coverage report\n- `nub run test:compile` - Focused alias for just the compile-mode project. Already covered by `nub run test`; use this when iterating on compile-related changes.\n- `nub run dev` - Execute TypeScript with Nub under source conditions\n- `nub run dev <file>` - Execute `<file>` with Nub & proper resolution conditions. Usually use for `play.ts`.\n- `nub run dev:play` - Quick alias to run play.ts for experimentation\n- `nub run check:comments` - Fail on stacked `//` comment lines (`--fix` joins them)\n- `nub run lint` - Run biome linter with auto-fix\n- `nub run format` - Format code with biome\n- `nub run fix` - Run both format and lint\n\n## Rules\n\n- Nub v0.8.3 and Node.js v24+ required; Nub provisions Node from `.nvmrc`\n- ES modules are used throughout (`\"type\": \"module\"`)\n- All tests must be written in TypeScript - never use JavaScript\n- Use `play.ts` for quick experimentation; use proper tests for all permanent test cases\n- Features without tests are incomplete - every new feature or bug fix needs test coverage\n- Don't skip tests due to type issues - fix the types instead\n- Test both success and failure cases with edge cases\n- Keep added tests as minimal and dense as possible without sacrificing comprehensiveness; avoid redundant assertions or broad fixtures when a focused case proves the behavior.\n- No log statements (`console.log`, `debugger`) in tests or production code\n- Never stack prose across consecutive `//` lines. Lines have no maximum width here — the editor wraps for display — so a paragraph split across several `//` lines is just a hard-wrapped line, and hard wrapping breaks search, diffs and editing. Write one long `//` instead. `nub run check:comments` enforces this in pre-commit and CI; `--fix` joins the offenders. Commented-out code, `@ts-`/`@__NO_SIDE_EFFECTS__`-style pragmas, bullet lists, and blocks separated by a bare `//` are exempt. When two adjacent comments describe two different statements, separate them with a blank line rather than joining them.\n- Keep comments SHORT AND TIGHT — one lowercase sentence fragment, one clause, no trailing period. Never a capitalized full sentence, and never two of them. Extreme concision: say only what the code cannot say, and cut the setup sentence, the recap, and the same point restated in different words; a comment that needs three sentences usually means the code should be clearer. Identifiers keep their real casing (`Error`, `parse()`), only the prose is lowercase.\n- Ask before generating new files\n- Use `util.defineLazy()` for computed properties to avoid circular dependencies\n- Never branch on specific schema types in shared code. No `def.type === \"optional\"` conditionals, no hardcoded lists of wrapper type names, no walks up the wrapper chain hunting for a particular type. Every schema type added later silently falls through such a check, and the list is wrong the moment someone writes a new wrapper. When a shared path needs to know something about a schema, express it as a structural property on the internals — `optin`/`optout`, `values`, `pattern`, `propValues` — and let each type declare its own answer. This is not negotiable in the parse paths; a PR that adds edge-case conditional logic keyed on schema types will be rejected regardless of how well it is tested.\n- Performance is critical - parameter reassignment is allowed for optimization\n- Any change to `packages/zod/src` must be weighed on **all three axes: runtime performance, memory consumption, and bundle size** — see \"The three axes\" below. A change that improves one and is only checked on that one is not finished.\n- ALWAYS use the `gh` CLI to fetch GitHub information (issues, PRs, etc.) instead of relying on web search or assumptions\n- Keep JSDoc as minimal as possible. A self-explanatory type or symbol name needs no doc comment. When a comment is genuinely required, write one short sentence describing behavior — not history, rationale, or examples. Don't add interface-level JSDoc that just restates the interface name.\n- When you've modified a PR (or opened/closed/commented on one), include the PR URL liberally in summary messages — at minimum once at the end of any reply that touched it\n- When creating a PR, do not include a separate test plan section in the body. Link to any relevant issues under discussion, and use the same copywriting guidelines from \"Commenting on issues and PRs\": concise maintainer voice, prose over templates, and validation details only when they are material to the reader.\n- Format validators (`z.iso.*`, `z.email()`, `z.url()`, `z.uuid()`, …) are deliberately narrower than the specs they're named after. \"The spec allows X\" is not a reason to accept X — see \"Format validators: spec compliance is not the bar\" below.\n- NEVER bump the version in `packages/zod/package.json` (or any package's `package.json`), and NEVER run `gh workflow run release.yml`. Publishing is a maintainer-only dispatch behind an approval. If a version bump is genuinely needed, ask first.\n\n## The three axes\n\nZod is judged on **runtime performance**, **memory consumption**, and **bundle size** at once, and they trade against each other constantly. Optimizing one in isolation is how regressions land: a change that buys bundle bytes can cost construction speed, and one that saves memory can cost both. Any non-trivial change to `packages/zod/src` — and any change to `core/` at all, since every build ships it — needs a number on all three before it is done. Report them together, including the ones that got worse.\n\n`zod/mini` deserves its own line on the bundle axis: it is sold on size, so a fixed cost lands very differently there (~200 B is 6% of the smallest mini bundle and 1% of classic's).\n\n| axis    | how to measure                                                                                                                                    |\n| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| runtime | `packages/bench/*.ts` via `nub run bench <name>`, plus construction cost — parse and construction move independently                                 |\n| memory  | `packages/bench/memory/schema-footprint.ts` and `realworld.ts` (retained bytes per schema; run under `--expose-gc`)                               |\n| bundle  | bundle a `packages/treeshake` fixture with esbuild `--minify` under `--conditions=@zod/source` and gzip it, for both a classic and a mini fixture |\n\n### Benchmarking traps\n\nThese produced hours of wrong conclusions; check them before believing a number.\n\n- **Check `uptime` first.** A loaded machine invents effects of ±16%. Numbers taken above a load average of ~8 are worthless.\n- **Interleave the two revisions.** Running all of A then all of B puts any drift entirely on whichever ran second. Alternating round-by-round took per-side spread from 2.5x to 4%.\n- **Loading two zod revisions into one process makes call sites polymorphic.** It is fine for comparing construction, unreliable for tight leaf parses.\n- **Allocating benchmarks can't be timed by a fixed-duration loop.** `z.array().parse()` allocates per call, so a time-boxed harness samples whatever the collector is doing — it gave +8.2% and −17.3% on consecutive identical runs. Use a fixed iteration count with `gc()` between samples and take the minimum.\n- **Make sure the work isn't optimized away.** If a micro-benchmark reports ~1e9 ops/sec, V8 eliminated the loop; consume the result.\n\n### Property placement, specifically\n\nMost of the memory in a schema is its own-property count, not its closures. V8 sizes an instance's backing store in steps and `$constructor` assigns nothing itself, so instances get no in-object slots: **≤12 own properties cost 128 bytes, 13–20 cost 848, ≥21 cost 1616** (`packages/bench/memory/prop-slack.ts` measures this). Members therefore live on the prototype and materialize per instance on first read.\n\nIf you touch that machinery, three things bite:\n\n- **Moving a property between definition sites changes its descriptor**, and writability/enumerability/configurability are part of the public contract. `packages/bench/memory/` has a surface-diff approach for this: dump every descriptor, alias and assign/delete semantic on both revisions and diff them. It caught four real regressions that tests did not.\n- **Redefining an accessor demotes the object to dictionary mode**, which cost 2x on `z.object().parse` once. Run `packages/bench/memory/dict-mode.ts` after any change here — every instance must report `fast`.\n- **A bound function pays a call-time trampoline.** Fine for cold builder methods, measurably not fine on the parse path.\n\n## Format validators: spec compliance is not the bar\n\nZod's format validators — `z.iso.*`, `z.email()`, `z.url()`, `z.uuid()`, and the rest — are named after specs but do not implement them. Each one matches the profile that real producers emit and real consumers accept, which is almost always far narrower than what the grammar permits. That narrowness is the product, not a gap in it.\n\nSo **\"the spec allows X, therefore Zod must accept X\" is not an argument**, and a PR that widens a format regex on that basis alone will be closed. The question is never what the spec permits; it's whether accepting the wider form helps more people than rejecting it hurts. A form that shows up in real payloads and breaks real integrations is worth fixing. A form that only appears when someone reads the grammar and constructs a string from it is not — when one of those reaches a validator in production it's a bug or an attack, and rejecting it is the useful behavior.\n\nThree things make a widening request an automatic no:\n\n- **The spec permits the form only \"by mutual agreement\" of the two parties.** A general-purpose validator is never party to that agreement, so it can't honor the clause. ISO 8601 expanded years (`+010000-01-01T00:00:00.000Z`) are the canonical case: the standard also requires the parties to agree the digit count, and runtimes disagree — JavaScript emits six digits, Java's `Instant` emits five, Python rejects both. Declined in #6154.\n- **The wider form breaks the JSON Schema output.** `z.iso.datetime()` and `z.iso.date()` emit `format: \"date-time\"` and `format: \"date\"`, which mean RFC 3339 — four-digit years, no sign. Widening those regexes makes `z.toJSONSchema()` emit a pattern that conformant consumers reject.\n- **The cost lands on everyone.** These regexes live in `core/regexes.ts`, so every added alternation is bytes in every bundle including `zod/mini`, plus more backtracking on a hot parse path. Weigh it on all three axes above before you argue for it at all.\n\nFor calibration on how narrow these already are: `z.iso.datetime()` rejects basic format (`20200101T061500Z`), week dates (`2020-W01-1`), ordinal dates (`2020-001`), comma decimal separators, and `24:00`. Every one of those is valid ISO 8601. None of them are going in either.\n\nPoint anyone who genuinely needs a wider format at `z.string().regex()` or `z.string().refine()`. That escape hatch is the answer, not a wider default.\n\n## Triaging issues and PRs\n\nFollow the `triage` skill at [`.claude/skills/triage/SKILL.md`](.claude/skills/triage/SKILL.md) whenever you're asked to investigate, triage, or form an opinion on an issue or PR. It is the single source of truth for the procedure. Claude and Codex both auto-discover it (Codex via the `.codex/skills` symlink); read it directly if your agent doesn't.\n\nFor a draft **security advisory** — a GHSA id, the Security tab, a private vulnerability report — use the `security-advisory` skill at [`.claude/skills/security-advisory/SKILL.md`](.claude/skills/security-advisory/SKILL.md) instead. It shares the conventions above but reorders the work: the fix lands on `main` before any reporter comment is drafted, since the comment's whole value is the PR it links to.\n\nWrite-ups live in the gitignored `.triage/` tree — one directory per ticket, `.triage/issues/NNNN/results.md` and `.triage/prs/NNNN/results.md`, with scratch files and repros alongside. When investigating a PR from its worktree, resolve the root repo first (`git worktree list | head -1`) and write results back there; a relative write lands in the worktree's own ignored `.triage/` where nobody will find it.\n\n## Iterating on a contributor PR in a worktree\n\nWhen asked to make changes on top of an open PR (e.g. as a maintainer review suggestion), use a worktree so `main` stays clean:\n\n```bash\n# 1. Fetch the PR as a local branch and create a worktree for it\ngit fetch origin pull/<N>/head:pr-<N>\ngit worktree add ~/.cursor/worktrees/zod/pr-<N> pr-<N>\ncd ~/.cursor/worktrees/zod/pr-<N>\nnub install --frozen-lockfile   # fast, Nub's store is shared across worktrees\n\n# 2. Look up the PR's head info — you'll need the contributor's fork URL\n#    and the head ref name to push back.\ngh pr view <N> --repo colinhacks/zod \\\n  --json headRefName,headRepositoryOwner,maintainerCanModify\n\n# 3. If maintainerCanModify is true, add the fork as a remote and push to\n#    the PR's head ref (NOT to your local branch name).\ngit remote add <contributor> git@github.com:<contributor>/zod.git\ngit push <contributor> pr-<N>:<headRefName>                 # first push\ngit push <contributor> pr-<N>:<headRefName> --force-with-lease   # for amends\n```\n\nNotes:\n\n- Do NOT use `gh pr checkout --detach` for this — it moves your _current_ working tree into detached HEAD instead of creating a worktree.\n- Husky pre-commit runs biome format/lint via lint-staged; pre-push runs the full vitest suite. Both are fast and act as a safety net — don't bypass with `--no-verify` unless you have a specific reason.\n- **Preserve contributor commits.** Never `git reset --hard` or otherwise rewrite history that erases the contributor's work, even if you're rewriting the actual change. They need to stay in the PR's commit list to get credit on the merged PR. If their approach was wrong, add a `Revert \"...\"` commit (or just a plain commit that undoes those lines) and then add your replacement commit on top. Force-pushing a single clobbering commit strips them from the GitHub contributors graph.\n- When done, clean up: `git worktree remove ~/.cursor/worktrees/zod/pr-<N>` and `git branch -D pr-<N>` (and optionally `git remote remove <contributor>`).\n\n## Commenting on issues and PRs\n\nWhen posting on a maintainer's behalf via `gh` (PR comments, issue comments, reviews), match the house tone. Load the `prose-writing` skill before you write any of it — it is the general copy guide, and this section layers on top of it. The register is authoritative and friendly — concise, not bubbly, not over-explaining, not effusive. Comments come from a maintainer handing down decisions, not negotiating them. Friendly does not mean deferential.\n\n- Exclamation points are fine in moderation, especially to soften a decline or close out a thread (\"Thanks for looking into this!\"). Don't stack them and don't sprinkle them through technical writeups.\n- Skip effusive praise: \"Great work\", \"Awesome\", \"Thanks so much for this\", \"Thanks for the careful writeup\", \"you clearly read the RFC\". A short flat-affect affirmation walks the line well — \"Good investigation.\" or \"Solid catch.\" with a period, no exclamation, no superlatives. Warmth otherwise comes from a short closer (\"Thanks for looking into this, though 👍\"), not a preamble that butters up the contributor before the decision.\n- No \"PTAL\", \"WDYT\", or sign-off flourishes asking the contributor to re-review changes the maintainer pushed on top. State what changed and the merge intent. (\"LGTM\" is fine as a literal verdict at the end of a substantive review, not as a sign-off.)\n- When the user gives you exact wording for a comment, use it verbatim (fixing only obvious typos). Do not \"improve\" their phrasing to match this style guide — their direct instruction wins.\n- Lead with the decision or action: \"Going to merge as-is.\" \"Closing this out.\" \"I'd be open to a top-level utility but not as a method.\" Then the reasoning.\n- First person, owned opinions. \"I don't think this should be a method.\" \"I'm hesitant to add this.\" Don't hide behind passive voice or \"we could perhaps consider\".\n- Speak with authority. No hedging (\"maybe\", \"I think perhaps\", \"if that's okay\"), no apologizing for decisions, no asking permission to land changes the maintainer has already made. Decisions are stated as decisions.\n- Be direct when declining, but not curt. \"out of scope\", \"behaving as intended\", \"this is more complicated than it looks\" — firm, with a concrete reason. A friendly closer (\"thanks for looking into this\") is fine.\n- Cross-reference by number: `#4433`, `commit 2f8414bc`, `merged in #5718`. Concrete and verifiable.\n- Length matches substance, and substance is almost never as much as you think. Two or three sentences closes most threads. How long you spent investigating has nothing to do with how long the reply should be — a week of benchmarking still closes in three sentences. The `triage` skill works this through against a real decline.\n- No file paths, line numbers, or symbol names unless the reader has to open that file. They turn a two-line decision into a report, and nobody asked you to show your work. Say what the code does instead — \"the parse context is per-call state now\", not \"`core/memoizer.ts:133` writes onto the context\". The detail belongs in the `.triage/` write-up.\n- Pick the one or two strongest reasons and write them as prose. Resist enumerating every objection in a bullet list — even when each point is fair, it reads as piling on. The strongest argument plus a concrete escape hatch (e.g. \"`z.email().max(254)` already does this\") is usually enough.\n- Don't lift informal or coarse phrasing from external sources (blog posts, issues, comments) into the maintainer voice, even in quotes. Paraphrase the substance — quoted-in-context still reads as the maintainer talking.\n- Use prose with inline backticks for symbols. Reach for fenced code blocks only when showing non-trivial code is genuinely clearer than describing it.\n- Skip emojis in substantive technical writeups. A small `👍` in a casual closer is good — it keeps a decline or sign-off sounding warm and friendly without leaning on praise.\n- Bot mentions are bare imperatives: `@pullfrog review`, `@pullfrog fix merge conflicts`, `@pullfrog re-review fresh.`\n- When pushing a follow-up on top of a contributor's PR, state what changed, why it differs from the original approach, and that the maintainer is merging. Never ask the contributor to review the maintainer's changes — they are final, not a proposal. Don't thank them for \"letting\" the maintainer rewrite their work.\n- When posting comments with code samples via `gh`, do NOT pass the body inline through a heredoc that requires escaping backticks. Backslash-escaped backticks (`` \\` ``) inside a `$(cat <<'EOF' ... EOF)` body get sent to GitHub literally and break inline code and template literals inside fenced blocks. Instead, write the comment to a file and pass it via `--body-file <path>` (for `gh pr/issue comment`) or `-F body=@<path>` (for `gh api`). This preserves backticks and `${...}` exactly as written.\n\n## Pushing to a new branch (don't accidentally push to main)\n\nWhen the user asks for a \"new PR\" or \"new branch\", the work has to land on a non-`main` ref on the remote. The footgun: `git worktree add <path> -b <branch> origin/main` (and `git checkout -b <branch> origin/main`) silently set the new branch's upstream to `refs/heads/main` because the start point is a remote-tracking ref. A subsequent `git push -u origin <branch>` then pushes to `refs/heads/main`, not to a new remote branch. Two of these in a row have happened.\n\nAvoid by being explicit on the first push:\n\n```bash\n# Always use a refspec on the initial push so the remote ref name is unambiguous.\ngit push -u origin <branch>:refs/heads/<branch>\n```\n\nThen verify the output. The line you want to see is:\n\n```\n* [new branch]      <branch> -> <branch>\n```\n\nIf the right-hand side says `main`, the push went to main — abort or revert.\n\nUse `git push origin HEAD:refs/heads/<branch>` for subsequent pushes if you didn't `-u` originally. Don't rely on `git push -u origin <branch>` alone — its behavior depends on the upstream config, which `worktree add -b ... origin/main` set wrong for you.\n","isInternal":false,"tokens":5171,"sizeBytes":22032},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"AGENTS.md","isInternal":false,"tokens":3,"sizeBytes":9},{"name":"SKILL.md","path":".claude/skills/security-advisory/SKILL.md","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/.claude/skills/security-advisory/SKILL.md","title":"security-advisory","category":"anthropic-skill","format":"markdown","content":"---\nname: security-advisory\ndescription: Triage a draft security advisory in colinhacks/zod — a private vulnerability report from the Security tab, named by GHSA id (GHSA-xxxx-xxxx-xxxx) or reached from \"the draft advisories\", \"the security reports\", \"the vulnerability queue\". Use instead of the `triage` skill whenever the ticket is an advisory rather than a public issue or PR, because the workflow is different: the report is private, the burden of proof is on the reporter, the fix must be LANDED before any comment is drafted, and the publish-or-decline call has its own calculus. Covers reproducing a PoC against main, the schema-vs-input test, deduplicating a cluster of reports into distinct defects, landing the fix first, the publish-vs-decline framework, per-reporter privacy rules that make a GHSA id unlinkable, and the results.md format under .triage/advisories/GHSA-xxxx-xxxx-xxxx/.\n---\n\n# Triaging a draft security advisory\n\nAdvisories are not issues with a scarier label. Four things differ, and each one changes the order you work in:\n\n- **The report is private**, and private in both directions — see `## Privacy cuts both ways`.\n- **The burden of proof is on the reporter.** Most submissions are scanner output or a rediscovery of intended behavior. A plausible writeup is not evidence.\n- **The fix lands before the comment exists.** See `## Land the fix first`. This is the rule most often broken and the one that wastes the most of Colin's time.\n- **Real defect and publishable vulnerability are different questions.** See `## Publish or decline`.\n\nEverything in the `triage` skill about where files live, `results.md` frontmatter, close-comment voice, and reporting back in chat still applies. This skill replaces only its `## Investigating a draft advisory` section.\n\n**Never accept, publish, decline, or comment on an advisory.** The write-up and the drafted note are the deliverables; the disposition is Colin's. Landing a fix is a code change and follows the repo's normal rules; publishing a CVE is not, and is never yours.\n\n## Reading the queue\n\n```bash\ngh api repos/colinhacks/zod/security-advisories --paginate \\\n  --jq '.[] | select(.state==\"triage\") | [.ghsa_id, .severity, .summary] | @tsv'\ngh api repos/colinhacks/zod/security-advisories/<GHSA-ID> --jq '.description'   # full report + PoC\n```\n\nWrite-ups go to `.triage/advisories/<GHSA-ID>/results.md`, scratch files beside them. Frontmatter swaps `number:` for `ghsa:` and adds `severity_claimed:` (what the reporter asserted) next to `severity_actual:` (yours, or `none`).\n\n`verdict` is one of `valid`, `not-a-vulnerability`, `already-fixed`, `duplicate` (with `duplicate_of:`), `needs-reporter-info`, `needs-maintainer-decision`. Add `fixed_by:` / `fix_pr:` once a fix lands, and `disposition_recommended:` when you have a publish-or-decline opinion. Keep `verdict` describing the defect and `disposition_recommended` describing what to do about it — do not let a decline quietly rewrite a real finding as `not-a-vulnerability`.\n\n## Order of work\n\n1. **Extract the PoC verbatim** into `.triage/advisories/<GHSA-ID>/poc.ts`. Do not clean it up first — a PoC that only works after you fix it is a finding in itself.\n2. **Reproduce against current `main`**: `nub run dev .triage/advisories/<GHSA-ID>/poc.ts`. If it does not reproduce, say so and check whether it ever did (`git log -S`, or test the version the reporter named).\n3. **Decide whether the reproduced behavior is a vulnerability at all.** The recurring question on this repo is whether the attacker controls the **schema** or only the **input**. A schema is application code, so a report needing an attacker-authored schema is usually `not-a-vulnerability`. For prototype reports: is the polluted prototype the returned object's own (contained) or `Object.prototype` itself (real)?\n4. **Enumerate reachability yourself; do not inherit the reporter's.** Reporters test the path they found. Grep every call site that can feed attacker data into the sink and build a route table — precondition per route, and whether each needs only input, or a particular schema shape, or cooperating application code. This is where the severity actually gets decided, and it routinely turns up both routes the reporter missed and routes that are already closed.\n5. **Dedupe the cluster.** Many reports describe one defect. `grep -rl '<keyword>' .triage/advisories/*/results.md`, pick a canonical, cross-link the rest with `duplicate_of`. Fold by underlying defect, never by title keyword.\n6. **Check for public prior art** — an open issue or PR describing the same bug without the security framing. If one exists the defect is already public, which kills the embargo and changes the disposition.\n7. **Land the fix** — see below.\n8. **Then** write `results.md` and draft the reporter note.\n\n## Land the fix first\n\nA close comment's entire value is the line that says where it was fixed. Draft one before a fix exists and you have written a decline, whatever it says — the reporter gets no commit to verify, no release to wait for, and no reason to believe the report was acted on. Colin then has to do the work you were asked to do before he can send anything.\n\nSo the fix is not a follow-up to the triage. It is a step inside it, and it comes before any comment is drafted:\n\n- **Search for existing PRs before writing one.** Popular defects attract near-duplicate community PRs — this repo had seven for a single formatter bug. Read every diff, rank them, and take the best as a base rather than opening an eighth.\n- **Judge the candidates against the whole defect, not the reported symptom.** A patch that fixes the one function in the PoC while three sibling call sites keep the bug is partial. Build a behavior matrix of the cases that matter, run it against `main` and against each candidate patch, and let that table pick the winner. This is also how you catch a patch that trades a crash for silent data loss.\n- **Extend the base rather than starting over**, and preserve the contributor's commits — AGENTS.md covers pushing to a contributor's head ref.\n- **Run what CI runs** before pushing, then confirm CI itself is green.\n- **Do not bump a version and do not cut a release.** Landing on `main` is the goal. Release timing on a security fix is Colin's, and on this repo approving the release workflow's `npm` environment is the one irreversible action.\n\nOnly once the fix is merged do you write the comment, and the comment names the merge commit and the PR.\n\nIf the fix cannot land — the right change is a breaking one, or the affected major is EOL — say so explicitly in `results.md` and draft the comment around what the reporter's actual remedy is. That is a real outcome, not a skipped step.\n\n## Privacy cuts both ways\n\nThe report body may contain an unpatched exploit, so it never goes into a public issue, PR, or commit message. A fix can land publicly while the advisory stays private — describe the defect neutrally in the commit and let the mechanism speak for itself.\n\nThe direction that gets missed: **each reporter can see only their own advisory thread.** A GHSA id in a comment is a dead link to everyone but the maintainers. A duplicate close must never say \"duplicate of GHSA-xxxx\" or \"see the reasoning on that thread\" — the reporter can open neither. Every comment stands alone: state the reasoning inline, and refer to a sibling report as \"a separate report of the same defect\", without the id and without the other reporter's name. Public issues, PRs and commit SHAs are the only cross-references a reporter can follow. The `duplicate_of` link belongs in `results.md`, which is for Colin.\n\n## Publish or decline\n\nA defect can be real and still not be worth a published GHSA. Answer these before recommending either way:\n\n- **Does the attacker control the value, or only the key?** A write the attacker cannot choose the value of — a fixed object, a truthiness flip — needs a second-order sink in the victim's own code before it does anything. That sink is the application's bug, not the library's.\n- **What actually happens at the boundary?** Test the near-miss cases rather than assuming. A gadget that throws on the interesting inputs and only pollutes on the boring ones is much weaker than the report claims.\n- **Is the supported major reachable without cooperating application code?** If the current version needs an unusual schema shape or an app that derives paths from user input, an advisory naming it overstates the exposure.\n- **Is there a patch to point at?** An advisory against an EOL major with no patch turns every install red in `npm audit`, mostly transitively, with \"upgrade the major\" as the only remedy. That is a large ecosystem cost for little defensive gain.\n- **Is it already public?** A defect with an open issue describing it was never embargoable. Fix it in the open.\n\nState the recommendation plainly and name what would reverse it. Where a reachability judgment rests on how common some application pattern is, say that you reasoned it rather than measured it.\n\nCredit is the thing to offer when declining. A reporter who found a real bug that produced a real fix wants attribution — release-notes credit costs nothing and is usually what they were after.\n\n## The reporter note\n\nPer the `triage` skill's `## Drafting the close comment`, plus one advisory-specific pressure: a decline invites you to walk the reporter through the entire severity argument, and the analysis is right there. Do not paste it. Four things belong in the note — it is fixed and where, whether you are publishing, the single reason that decides it, and the offer of credit. Everything else lives in `results.md`.\n\nCorrect a reporter's overstated impact when it is wrong, in one sentence, with what you actually observed. Do not relitigate their CVSS vector line by line.\n","isInternal":false,"tokens":2222,"sizeBytes":9873},{"name":"SKILL.md","path":".claude/skills/triage/SKILL.md","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/.claude/skills/triage/SKILL.md","title":"triage","category":"anthropic-skill","format":"markdown","content":"---\nname: triage\ndescription: Investigate a GitHub issue or pull request in colinhacks/zod and write up a durable verdict. Use whenever asked to triage, investigate, review, evaluate, or form an opinion on an issue or PR (by number, URL, or \"the open PR queue\"), and when sweeping many of them in bulk. For a draft SECURITY ADVISORY (a GHSA id, the Security tab, a private vulnerability report) use the `security-advisory` skill instead — it shares this file's conventions but its workflow lands the fix before drafting any comment. Covers where write-ups live on disk (.triage/issues/NNNN/, .triage/prs/NNNN/), the cheap-disqualifier pass that avoids spinning up a worktree for stale PRs, the worktree checkout procedure for PRs, the results.md format that keeps a 250-PR sweep greppable and resumable, how to report back in chat — the final message is all the user sees, so it leads with a code block showing the problem and a short outline of the fix — and the maintainer-voice comment it drafts whenever the verdict is to close.\n---\n\n# Investigating issues and PRs\n\nProduce a write-up a maintainer can act on without re-reading the thread. The durable deliverable is always a file on disk — sweeps span many sessions and agents, so anything living only in a transcript is lost. But the file is not what the user reads first: your final chat message is, and for most tickets it is the only thing they will ever read. Both matter, and `## Reporting back` below is not optional.\n\n**Read-only on GitHub.** Never comment, label, assign, close, review, approve, or merge unless the user explicitly asks in this session. You are forming an opinion, not publishing one. Drafting the close comment described below is part of forming it — writing the text is required, sending it is not yours to do.\n\n## Where things live\n\nEverything goes under `.triage/` in the **root repo**, which is gitignored — write freely, it never shows up in `git status`.\n\n```\n.triage/\n  index.md                    # the original issue sweep index — hand-maintained prose, still the canonical issue queue\n  issues/index.md             # generated from frontmatter by reindex.mjs\n  issues/<N>/results.md       # the write-up; scratch files live beside it\n  prs/index.md                # generated\n  prs/<N>/results.md\n  advisories/<GHSA-ID>/results.md   # draft security advisories from the Security tab\n  signal-report.md            # generated by scripts/triage-signal.ts — ranks the open backlog\n```\n\nOne directory per ticket, named by bare GitHub number (no leading zeros, no `issue-`/`pr-` prefix) — or by bare GHSA id for an advisory. `results.md` is the write-up. Everything else you need — repro scripts, a saved `gh pr diff`, test fixtures, benchmark output, notes — goes in that same directory. Name scratch files descriptively (`repro.ts`, `diff.patch`, `bench-before.txt`); there is no naming rule beyond not colliding with `results.md`.\n\nFor an issue repro, `nub run dev .triage/issues/<N>/repro.ts` from the root repo works directly.\n\nA repro must sit **inside** the checkout it is testing, because module resolution walks up from the file, not from the cwd — a script under `.triage/` cannot resolve `zod` from a PR worktree. Keep the canonical copy in the ticket directory and copy it in to run: `cp .triage/prs/<N>/repro.ts <worktree>/repro-<N>.ts`. That is also what lets you run the identical file against both `main` and the PR and diff the output, which is the cheapest way to characterize a behavior change. For a probe needing GC or other flags, invoke Nub with the flags directly: `nub --expose-gc --conditions=@zod/source repro-<N>.ts`.\n\n### Writing to the root repo from a PR worktree\n\nThis is the one mistake that silently destroys work. A worktree has its **own** gitignored `.triage/`, so a relative write from inside it lands somewhere nobody will ever look.\n\nResolve the root once and always write through it:\n\n```bash\nZOD_ROOT=$(git worktree list | head -1 | awk '{print $1}')   # correct from inside any worktree\nmkdir -p \"$ZOD_ROOT/.triage/prs/<N>\"\n```\n\nEvery path you write must start with `$ZOD_ROOT`. Investigate in the worktree, record in the root.\n\n## results.md format\n\nOpen with YAML frontmatter. It is what makes a 250-PR sweep aggregatable and resumable — an interrupted sweep is resumed by grepping `status`, never by re-reading write-ups.\n\n```yaml\n---\nnumber: 5928\ntype: pr                    # pr | issue\nstatus: done                # in-progress | done\nverdict: merge-with-changes\ntitle: \"fix: preserve brand through .optional()\"\nauthor: someuser\nurl: https://github.com/colinhacks/zod/pull/5928\ninvestigated: 2026-08-08\n---\n```\n\n`verdict` for **PRs** — one of:\n\n- `merge-as-is` — correct, tested, in scope. Say so plainly.\n- `merge-with-changes` — right idea, needs specific edits. Enumerate them concretely enough to apply.\n- `needs-author-changes` — blocked on something only the author can supply (repro, rebase, scope cut).\n- `decline` — out of scope or wrong approach. Give the one or two strongest reasons, plus the escape hatch users actually have.\n- `superseded` — already fixed on `main` or landed via another PR. Name the commit or PR.\n- `needs-maintainer-decision` — the code is fine; the call is a product/API judgment that is Colin's to make. State the tradeoff and your recommendation.\n\n`verdict` for **issues** — reuse the vocabulary already established in `.triage/index.md`: `good-idea`, `already-fixed`, `needs-maintainer-input`, `not-recommended`, `needs-more-reproduction`, `duplicate` (add a `duplicate_of: NNNN` key alongside it).\n\n`verdict` for **advisories** — one of:\n\n- `valid` — a real vulnerability in zod, reproducible on current `main`. State the affected versions and the fix.\n- `not-a-vulnerability` — the behavior is intended, requires the app to already be doing something unsafe, or is a plain bug with no security consequence. This is the most common outcome; say which.\n- `already-fixed` — real, but no longer reproduces on `main`. Name the commit or release that fixed it.\n- `duplicate` — the same defect as another advisory (add `duplicate_of: GHSA-xxxx-xxxx-xxxx`). The `__proto__` reports cluster heavily; check before concluding.\n- `needs-reporter-info` — the report is too vague to reproduce, or the PoC does not run.\n- `needs-maintainer-decision` — real behavior, but whether it counts as a vulnerability is a threat-model call that is Colin's to make.\n\nAdvisory frontmatter swaps `number:` for `ghsa:` and adds `severity_claimed:` (what the reporter asserted) alongside `severity_actual:` (your assessment, or `none`).\n\nSet `status: in-progress` in a stub the moment you start, so a parallel agent or a later session does not duplicate the work. Flip to `done` before you report back.\n\n**One pass. Finish the ticket.** A maintainer question is never a reason to leave a write-up unfinished — investigate fully, commit to a recommended `verdict`, write every section including `## Proposed comment`, and flip `status: done`. Anything genuinely needing the maintainer goes in `## Open questions` (below) *inside the finished file*, so their answer is a one-line amendment to a complete write-up rather than a second investigation. Never hand back a provisional verdict, a `status: in-progress` file, or a promise to finalize later; never wait on an answer before writing. `verdict: TBD` is not a verdict. The only thing that legitimately blocks `done` is evidence you could not obtain — a PR that will not build, a repro that needs credentials — and that is `needs-author-changes` or `needs-more-reproduction`, which are verdicts, not deferrals.\n\nAfter the frontmatter, write prose under these headings. Match the density of the existing issue write-ups in `.triage/issues/*/results.md` — they are the house style; skim one before your first write-up.\n\n- `## Request` / `## Change` — what is actually being asked or changed, in your own words. Include the thread's evolution when maintainers or the author already moved the discussion.\n- `## Current state` — what the code on `main` does **today**, with `file.ts:line` references. Most old tickets were filed against v3 or early v4 and are stale on arrival; this section is usually where the verdict comes from.\n- `## Analysis` — correctness, edge cases, type-level implications, perf, and whether the tests actually cover the claim.\n- `## Recommendation` — the action to take, and the substance of the reply to post. Do not draft it in maintainer voice unless asked, with one standing exception below.\n- `## Proposed comment` — **required whenever the recommendation is to close the ticket**, in a fenced block, ready to paste. See below.\n- `## Open questions` — **only** for calls that are genuinely the maintainer's: product/API direction, a breaking-change tradeoff, taste, or picking between two contributors' PRs. Omit the heading entirely when there are none, which is the common case. Never park a question here that the code, the tests, `git log`, or an existing write-up answers — that is research you owe, not a decision they owe. See below.\n\n### Writing `## Open questions`\n\nEach question is answerable cold, by someone who has not read the write-up and will not open it. One line for what is at stake, then 2–4 lettered options, each with a one-line consequence, with your recommendation marked and placed first. Write in behavior, not identifiers: \"should a schema that declares a field named `__proto__` still reject it?\" not \"should `handleCatchall` precede the `keySet` check?\".\n\nThe verdict above already assumes your recommended option. Say which verdict each *other* option would produce, so the maintainer can see the cost of disagreeing without asking you to redo the analysis:\n\n```markdown\n## Open questions\n\n**Q1 — Should `.strict()` reject an own `__proto__` key, or keep silently dropping it?** Runtime breaking change for anyone posting JSON bodies through `.strict()`.\n\n- **(A) Report it — take the PR.** ← recommended. Restores \"strict rejects every unknown key\". Verdict as written: `merge-with-changes`.\n- (B) Keep dropping it, document the exemption. Zero breakage; the one key an attacker actually sends stays the one key strict does not flag. → `decline`.\n- (C) Report it behind a config flag. No breakage; a permanent knob for a two-line behavior. → `needs-author-changes`.\n```\n\nWhen the answer arrives, amend the file in place: set the final `verdict`, fold the decision into `## Recommendation`, rewrite `## Proposed comment` to match, and leave the question and its answer recorded so nobody relitigates it. `status` stays `done` throughout — it was never not done.\n\n### Drafting the close comment\n\nAny verdict whose action is \"close this\" gets a drafted reply: `decline` and `superseded` on a PR, `not-recommended`, `already-fixed`, and `duplicate` on an issue, and `not-a-vulnerability`, `already-fixed`, and `duplicate` on an advisory. Closing is the one outcome where a human is going to have to say something to a real person who spent time on this, so writing that sentence is part of the triage, not a follow-up task.\n\nFollow the \"Commenting on issues and PRs\" section of AGENTS.md for the maintainer voice. Succinct, conversational, friendly. Lead with the decision, give the one or two strongest reasons, name the escape hatch the user actually has, and close warmly. Two or three sentences is usually the whole thing.\n\nFor an issue reporting that `z.coerce.number()` accepts `null`, the whole draft is:\n\n```markdown\nWorking as intended. `z.coerce.number()` runs `Number()` on the input before it validates, and `Number(null)` is `0` — the coercion happens first, so there is nothing left to reject by the time the number check runs.\n\nIf you want these rejected, use plain `z.number()` and do the coercion at the boundary where you actually know the input is a numeric string. Closing this out — thanks for the writeup 👍\n```\n\nNote what it does not do: no bullet list of every objection, no apology, no \"great question\", no asking whether the reporter agrees. Decision, reason, escape hatch, closer.\n\n#### The investigation's depth never sets the comment's length\n\nThis is the rule that gets broken, and it gets broken hardest on exactly the tickets where the investigation went deepest. A week of benchmarks, a prior-art trail, a prototype, an ecosystem comparison — all of it feels load-bearing, so it leaks into the reply and the two-or-three-sentence bar quietly becomes five paragraphs. **The evidence you are proudest of is the first thing to cut.** It belongs in `results.md`, which is where a maintainer looks when someone pushes back; the comment is not the place to prove you did the work.\n\nA worked case. A draft advisory reported ReDoS in `z.fromJSONSchema()` — real, reproducible, two sinks, exponential. The investigation ran long: measured timings, prior art in a closed issue and a closed fix PR, a 54MB dependency weighed, a zero-dep detector prototyped and evaluated, Ajv's published security contract pulled in as precedent. The drafted comment carried most of that. What the maintainer actually posted, in full:\n\n```markdown\nClosing this out — declining. `z.fromJSONSchema` is designed to convert arbitrary JSON Schema to a Zod schema, and it up to the user to do their own out of band validation. Thanks for the report.\n```\n\nThree sentences, and **this is what most declining comments should look like.** Read what it drops and adopt each as a rule:\n\n- **No cross-references.** No issue number, no PR number, no prior-art trail — even when a maintainer closed the identical report before. That history justifies the decision to *you*; it does not need to be shown to the reporter.\n- **No numbers, no engineering.** No timings, no dependency sizes, no analysis of what a guard would cost or why the cheap version fails. None of it changes the answer.\n- **No external precedent.** How another library documents the same contract is research that settles your verdict, not material for the reply.\n- **The reason is design intent, asserted — not an argument, defended.** \"Is designed to convert arbitrary JSON Schema\" states the contract in one clause and stops. A maintainer declares scope; they do not litigate it. Once you find yourself building a case, you have written the wrong document.\n- **No concession and no promise.** Do not offer \"you have a fair point about X\", and do not commit to a docs change, a new option, or any follow-up. Real follow-ups get filed as their own issue; a decline that promises work is no longer a decline.\n\nKeep only: the decision, one clause of reason, what the user does instead (\"their own out of band validation\"), and a short closer. When a drafted comment runs past three or four sentences, that is the signal to cut, not to reorganize.\n\nDrafting is not posting. The draft sits in `results.md` and in your chat message; you never send it unless the user explicitly asks in this session. For an advisory the draft is the note to the reporter, and it must not restate an unpatched exploit.\n\nThe length rule binds hardest on an advisory decline, which is where it is most often broken: a reporter who filed a real, reproducible defect invites you to walk them through the whole severity argument, and the analysis you just spent hours on is right there. Do not paste it. The reporter needs four things — it is fixed and where, you are not publishing, the one reason that actually decides it, and the offer of credit. Everything else belongs in `results.md`. A decline that runs to six paragraphs is a worse decline, not a more rigorous one.\n\n## Reporting back\n\n`results.md` is the archive. **Your final chat message is the interface, and for most tickets it is the only thing the user will ever read.** Assume they see it hours later, in a queue, without the thread in front of them, without opening the file, and without any of your working context. \"Investigated #5928, write-up is in `.triage/prs/5928/results.md`\" has delivered nothing — it just moved the work onto them.\n\nSo the last message before you rest always carries these three things, in this order, plus a fourth when the verdict is to close:\n\n**1. The problem, as a code block.** Not a paragraph describing the code block — the actual minimal snippet, with the wrong result written next to it as a comment. This is the single highest-value thing in the message: it is what lets the user confirm or reject your read in five seconds instead of re-deriving the whole ticket. Strip the reporter's app scaffolding down to the schema and the one call that misbehaves, and show actual versus expected.\n\n```ts\nconst schema = z.coerce.number();\n\nschema.parse(null); // → 0\nschema.parse(\"\"); // → 0\nschema.parse([]); // → 0\n// reporter expected each of these to fail validation\n```\n\nType-level tickets get the same treatment, with the inferred type as a comment instead of the parse result:\n\n```ts\ntype Out = z.infer<typeof schema>;\n// actual:   string | undefined\n// expected: string\n```\n\n**2. The shape of the fix, in two or three sentences.** Where it goes (`file.ts:line`), what changes, and how big it really is — one-line type tweak, new method on `ZodType`, or a refactor that touches every wrapper. If the fix is not worth doing, say what the user does instead. Do not paste the patch; the user is deciding whether to spend time here, not applying it from chat.\n\n**3. The verdict and the path.** The frontmatter `verdict` verbatim, plus a link to the ticket and the `results.md` path for the detail you left out.\n\n**4. The proposed comment, if the verdict is to close.** Repeat the `## Proposed comment` draft in the message, in its own fenced block, so the user can read and paste it without opening the file. Say plainly that you have not posted it.\n\n**5. The open questions, if the write-up has any.** Repeat them from `## Open questions`, options and all, so they are answerable without opening the file. Frame them as what they are — the ticket is triaged and the verdict is recorded; the answer only confirms or redirects it. Never present them as a request for permission to finish, never say the write-up is waiting on them, and never end on \"send me your answer and I'll finalize\". The work is already done.\n\nKeep the whole thing short — the code block plus a few sentences. The message is a summary that stands alone, not a condensed replay of the write-up, and it is not the place for the investigation narrative, the commands you ran, or the branches you ruled out. Those are what the file is for.\n\nTwo adjustments by shape of the task:\n\n- **Nothing reproduced** — an `already-fixed` or `not-a-vulnerability` verdict still gets a code block. Show the reporter's own snippet and the *correct* output it now produces on `main`; that is the evidence, and without it the user just has your word.\n- **A bulk sweep** — one summary table of ticket, verdict, and a one-line reason, and code blocks only for the two or three tickets that genuinely need a decision. Twenty code blocks is the same as none. Every close still gets its drafted comment written into its own `results.md`; the message just says which tickets have one waiting instead of inlining them all.\n- **More than one ticket has a comment to post** — pair each one: the link on its own line, its fenced comment block **directly underneath it**, then the next link. Never collect the links in one place and the comment blocks in another, and never make the user match a block to a link by reading the prose between them. This is how a set of draft advisories is always handed back, since the user posts them one at a time with the link and its text side by side.\n\n  ```markdown\n  ### [GHSA-xxxx-xxxx-xxxx](https://github.com/colinhacks/zod/security/advisories/GHSA-xxxx-xxxx-xxxx) — valid\n\n  <the comment for that advisory, in its own fenced block>\n\n  ### [GHSA-yyyy-yyyy-yyyy](https://github.com/colinhacks/zod/security/advisories/GHSA-yyyy-yyyy-yyyy) — duplicate\n\n  <the comment for that advisory, in its own fenced block>\n  ```\n\n## Investigating an issue\n\nNo worktree — read the code on `main`.\n\n1. `gh issue view <N> -R colinhacks/zod --comments` and read the whole thread, including maintainer replies. A prior decision from Colin usually settles it.\n2. Check for duplicates and prior art: `gh search issues --repo colinhacks/zod '<keywords>'`, and grep `.triage/issues/*/results.md` — the sweep may already cover it. Cross-link with a relative `../NNNN/results.md`.\n3. Reproduce against current source before believing the report. Write the repro to `.triage/issues/<N>/repro.ts` and run `nub run dev` on it. A large share of open issues are already fixed.\n4. Brainstorm before concluding. Consider the fix, its blast radius, whether it belongs in core at all, and what userland escape hatch already exists.\n5. Write `results.md`, then report back per `## Reporting back`.\n\n## Investigating a draft advisory\n\n**Use the `security-advisory` skill instead — invoke it before you start.** Draft advisories live in the Security tab, so nothing in `gh issue list` surfaces them, and the workflow diverges from this one in ways that matter: the report is private in both directions, the burden of proof sits with the reporter, the fix must be **landed before any comment is drafted**, and \"real defect\" and \"publishable vulnerability\" are separate calls.\n\nWhat carries over from this skill: where files live, the `results.md` frontmatter and headings, the close-comment voice, and `## Reporting back`. Advisory write-ups go to `.triage/advisories/<GHSA-ID>/results.md` with `ghsa:` in place of `number:`.\n\nNever accept, publish, decline, or comment on an advisory. The write-up and the drafted note are the deliverables; the disposition is Colin's.\n\n## Investigating a PR\n\nCheap pass first — most of a 250-PR backlog is resolved without ever checking out code.\n\n```bash\ngh pr view <N> -R colinhacks/zod --json number,title,author,state,isDraft,mergeable,mergeStateStatus,baseRefName,headRefName,headRepositoryOwner,maintainerCanModify,createdAt,updatedAt,additions,deletions,changedFiles,labels,comments\ngh pr diff <N> -R colinhacks/zod\n```\n\nDisqualify early and write the write-up straight from this, no worktree, when the diff is small and self-evident, or when the PR targets v3 / duplicates a landed change / is already superseded by `main` / is abandoned with unresolved conflicts. Verify `superseded` against actual code on `main` — do not infer it from dates.\n\nOtherwise check it out (this repo's convention, per AGENTS.md):\n\n```bash\ngit fetch origin pull/<N>/head:pr-<N>\ngit worktree add ~/.cursor/worktrees/zod/pr-<N> pr-<N>\ncd ~/.cursor/worktrees/zod/pr-<N>\nnub install --frozen-lockfile      # fast, Nub's store is shared\n```\n\nDo **not** use `gh pr checkout --detach` — it detaches your current working tree instead of creating a worktree.\n\nThen, in the worktree:\n\n1. Read the diff in full context, not just the hunks. What did it touch that the author did not think about?\n2. `git log --oneline main..HEAD` — is this rebased on anything current, or written against a version of the code that has since moved?\n3. Run the tests the change implicates: `nub exec --node vitest run <path>`. Run the full suite only when the change is broad.\n4. Judge test coverage against the repo's own bar: a feature or bug fix without a test is incomplete, and tests must be TypeScript.\n5. Probe the edge cases the author's tests skip — write them into `$ZOD_ROOT/.triage/prs/<N>/` and run them. A passing suite over a shallow test is not evidence.\n6. Write `$ZOD_ROOT/.triage/prs/<N>/results.md`, then report back per `## Reporting back`.\n\n**Clean up when done**, or worktrees accumulate across a large sweep:\n\n```bash\ncd \"$ZOD_ROOT\"\ngit worktree remove ~/.cursor/worktrees/zod/pr-<N> && git branch -D pr-<N>\n```\n\nKeep the worktree only when the verdict is `merge-with-changes` and you have been asked to push the changes; AGENTS.md covers pushing back to a contributor's fork, including preserving their commits.\n\n## Sweeping in bulk\n\nSeed the queue, then work it:\n\n```bash\ngh pr list -R colinhacks/zod --state open --limit 400 --json number,title,updatedAt\n```\n\nResume by asking disk, not memory:\n\n```bash\ngrep -l 'status: done' .triage/prs/*/results.md | wc -l                    # progress\ngrep -h '^verdict:' .triage/prs/*/results.md | sort | uniq -c              # verdict spread\nnub --node .claude/skills/triage/scripts/reindex.mjs prs             # regenerate .triage/prs/index.md\n```\n\n`reindex.mjs` rebuilds an index table from frontmatter, so the index is derived and never hand-edited for PRs. Pass `issues`, `prs`, or `advisories`.\n\n`nub run dev scripts/triage-signal.ts` regenerates `.triage/signal-report.md`, which ranks the whole open backlog by engagement, flags security content, and groups likely-duplicate clusters. Start a sweep there rather than at the top of `gh pr list` — roughly a third of the open backlog is duplicates, and the clusters collapse many tickets into one decision.\n\nWhen parallelizing across sub-agents, give each agent a disjoint set of numbers and have each write only its own ticket directories. Never have two agents share a results file.\n","frontmatter":{"name":"triage","description":"Investigate a GitHub issue or pull request in colinhacks/zod and write up a durable verdict. Use whenever asked to triage, investigate, review, evaluate, or form an opinion on an issue or PR (by number, URL, or \"the open PR queue\"), and when sweeping many of them in bulk. For a draft SECURITY ADVISORY (a GHSA id, the Security tab, a private vulnerability report) use the `security-advisory` skill instead — it shares this file's conventions but its workflow lands the fix before drafting any comment. Covers where write-ups live on disk (.triage/issues/NNNN/, .triage/prs/NNNN/), the cheap-disqualifier pass that avoids spinning up a worktree for stale PRs, the worktree checkout procedure for PRs, the results.md format that keeps a 250-PR sweep greppable and resumable, how to report back in chat — the final message is all the user sees, so it leads with a code block showing the problem and a short outline of the fix — and the maintainer-voice comment it drafts whenever the verdict is to close."},"isInternal":false,"tokens":5965,"sizeBytes":25278},{"name":"settings.json","path":"packages/docs/.vscode/settings.json","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/packages/docs/.vscode/settings.json","title":".vscode Skill","category":"anthropic-skill","format":"json","content":"{\n  \"editor.defaultFormatter\": \"biomejs.biome\",\n  \"editor.formatOnSave\": true,\n  \"editor.codeActionsOnSave\": {\n    \"source.organizeImports.biome\": \"explicit\",\n    \"source.fixAll\": \"always\",\n    \"quickfix.biome\": \"explicit\"\n  },\n  \"editor.wordWrap\": \"wordWrapColumn\",\n  \"editor.wordWrapColumn\": 120,\n  \"[mdx]\": {\n    \"editor.defaultFormatter\": \"unifiedjs.vscode-mdx\",\n    \"editor.formatOnSave\": true\n  }\n}\n","frontmatter":{"editor.defaultFormatter":"biomejs.biome","editor.formatOnSave":true,"editor.codeActionsOnSave":{"source.organizeImports.biome":"explicit","source.fixAll":"always","quickfix.biome":"explicit"},"editor.wordWrap":"wordWrapColumn","editor.wordWrapColumn":120,"[mdx]":{"editor.defaultFormatter":"unifiedjs.vscode-mdx","editor.formatOnSave":true}},"isInternal":false,"tokens":120,"sizeBytes":405},{"name":"meta.json","path":"packages/docs/content/meta.json","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/packages/docs/content/meta.json","title":"Content Skill","category":"anthropic-skill","format":"json","content":"{\n  \"root\": true,\n  \"pages\": [\n    \"---Documentation---\",\n    \"index\",\n    \"basics\",\n    \"api\",\n    \"error-customization\",\n    \"error-formatting\",\n    \"metadata\",\n    \"json-schema\",\n    \"codecs\",\n    \"compile\",\n    \"ecosystem\",\n    \"library-authors\",\n    \"---Packages---\",\n    \"packages/zod\",\n    \"packages/mini\",\n    \"packages/core\"\n  ]\n}\n","frontmatter":{"root":true,"pages":["---Documentation---","index","basics","api","error-customization","error-formatting","metadata","json-schema","codecs","compile","ecosystem","library-authors","---Packages---","packages/zod","packages/mini","packages/core"]},"isInternal":false,"tokens":97,"sizeBytes":340},{"name":"package.json","path":"packages/docs/package.json","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/packages/docs/package.json","title":"@zod/docs","category":"anthropic-skill","format":"json","content":"{\n  \"name\": \"@zod/docs\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"nub exec --node next build --turbopack\",\n    \"dev\": \"nub exec --node next dev\",\n    \"start\": \"nub exec --node next start\",\n    \"postinstall\": \"nub exec --node fumadocs-mdx\"\n  },\n  \"dependencies\": {\n    \"@inkeep/cxkit-react\": \"^0.5.117\",\n    \"@radix-ui/react-accordion\": \"^1.2.4\",\n    \"@vercel/analytics\": \"^1.5.0\",\n    \"@vercel/og\": \"^0.6.8\",\n    \"fumadocs-core\": \"15.1.0\",\n    \"fumadocs-mdx\": \"11.5.6\",\n    \"fumadocs-ui\": \"15.1.0\",\n    \"gray-matter\": \"^4.0.3\",\n    \"lucide-react\": \"^0.483.0\",\n    \"next\": \"15.5.25\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-tweet\": \"^3.3.1\",\n    \"remark\": \"^15.0.1\",\n    \"remark-gfm\": \"^4.0.1\",\n    \"remark-mdx\": \"^3.1.0\",\n    \"remark-stringify\": \"^11.0.0\",\n    \"shiki\": \"^3.8.1\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"^4.2.4\",\n    \"@types/mdx\": \"^2.0.13\",\n    \"@types/node\": \"22.13.10\",\n    \"@types/react\": \"^19.0.10\",\n    \"@types/react-dom\": \"^19.0.4\",\n    \"postcss\": \"^8.5.12\",\n    \"tailwindcss\": \"^4.2.4\",\n    \"typescript\": \"^5.8.2\"\n  }\n}\n","frontmatter":{"name":"@zod/docs","version":"0.0.0","private":true,"scripts":{"build":"nub exec --node next build --turbopack","dev":"nub exec --node next dev","start":"nub exec --node next start","postinstall":"nub exec --node fumadocs-mdx"},"dependencies":{"@inkeep/cxkit-react":"^0.5.117","@radix-ui/react-accordion":"^1.2.4","@vercel/analytics":"^1.5.0","@vercel/og":"^0.6.8","fumadocs-core":"15.1.0","fumadocs-mdx":"11.5.6","fumadocs-ui":"15.1.0","gray-matter":"^4.0.3","lucide-react":"^0.483.0","next":"15.5.25","react":"^19.0.0","react-dom":"^19.0.0","react-tweet":"^3.3.1","remark":"^15.0.1","remark-gfm":"^4.0.1","remark-mdx":"^3.1.0","remark-stringify":"^11.0.0","shiki":"^3.8.1"},"devDependencies":{"@tailwindcss/postcss":"^4.2.4","@types/mdx":"^2.0.13","@types/node":"22.13.10","@types/react":"^19.0.10","@types/react-dom":"^19.0.4","postcss":"^8.5.12","tailwindcss":"^4.2.4","typescript":"^5.8.2"}},"isInternal":false,"tokens":455,"sizeBytes":1112},{"name":"tsconfig.json","path":"packages/docs/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/packages/docs/tsconfig.json","title":"Docs Skill","category":"anthropic-skill","format":"json","content":"{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"paths\": {\n      \"@/.source\": [\"./.source/index.ts\"],\n      \"@/*\": [\"./*\"]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"types\": [\"mdx\", \"node\", \"react\", \"react-dom\", \"@vercel/og\"]\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\", \"components/if.tsx\", \"types/**/*.d.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n","frontmatter":{"compilerOptions":{"target":"ESNext","lib":["dom","dom.iterable","esnext"],"allowJs":true,"skipLibCheck":true,"strict":true,"forceConsistentCasingInFileNames":true,"noEmit":true,"esModuleInterop":true,"module":"esnext","moduleResolution":"bundler","resolveJsonModule":true,"isolatedModules":true,"jsx":"preserve","incremental":true,"paths":{"@/.source":["./.source/index.ts"],"@/*":["./*"]},"plugins":[{"name":"next"}],"types":["mdx","node","react","react-dom","@vercel/og"]},"include":["next-env.d.ts","**/*.ts","**/*.tsx",".next/types/**/*.ts","components/if.tsx","types/**/*.d.ts"],"exclude":["node_modules"]},"isInternal":false,"tokens":243,"sizeBytes":793},{"name":"tsconfig.test.json","path":"packages/docs/tsconfig.test.json","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/packages/docs/tsconfig.test.json","title":"Docs Skill","category":"anthropic-skill","format":"json","content":"{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"types\": [\"node\", \"vitest\"]\n  },\n  \"include\": [\"**/*.test.ts\"]\n}\n","frontmatter":{"extends":"./tsconfig.json","compilerOptions":{"types":["node","vitest"]},"include":["**/*.test.ts"]},"isInternal":false,"tokens":40,"sizeBytes":126},{"name":"vercel.json","path":"packages/docs/vercel.json","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/packages/docs/vercel.json","title":"Docs Skill","category":"anthropic-skill","format":"json","content":"{\n  \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n  \"installCommand\": \"bash -o pipefail -c 'curl -fsSL https://raw.githubusercontent.com/nubjs/nub/0ae8783f1f93763c56dfc827892cfb679e8a0a77/install.sh | bash -s -- 0.8.3 && export PATH=\\\"$HOME/.nub/bin:$PATH\\\" && cd ../.. && nub install --frozen-lockfile'\",\n  \"buildCommand\": \"export PATH=\\\"$HOME/.nub/bin:$PATH\\\" && nub run build\"\n}\n","frontmatter":{"$schema":"https://openapi.vercel.sh/vercel.json","installCommand":"bash -o pipefail -c 'curl -fsSL https://raw.githubusercontent.com/nubjs/nub/0ae8783f1f93763c56dfc827892cfb679e8a0a77/install.sh | bash -s -- 0.8.3 && export PATH=\"$HOME/.nub/bin:$PATH\" && cd ../.. && nub install --frozen-lockfile'","buildCommand":"export PATH=\"$HOME/.nub/bin:$PATH\" && nub run build"},"isInternal":false,"tokens":134,"sizeBytes":388},{"name":"README.md","path":"packages/bench/typia-case/README.md","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/packages/bench/typia-case/README.md","title":"typia-case Documentation","category":"plugin-manifest","format":"markdown","content":"# typia case\n\ntypia generates its validators with a TypeScript transformer, so this directory holds a prebuilt artifact rather than a source import. `build/index.cjs` is generated from `src/index.ts`.\n\nThe transformer does not run inside this workspace: `tspc` live mode crashes on typia's file transformer, and the alternative — `ts-patch install` — patches the TypeScript that every package here shares. The artifact is therefore built in a throwaway project and copied in. The generated file inlines every check, so it depends only on the `typia` runtime already in this package.\n\nTo regenerate:\n\n```sh\nmkdir -p /tmp/typia-build/src && cd /tmp/typia-build\nnub init -y --js --no-install --no-git\nnub add typia@9.7.2 typescript@5.9.3 ts-patch@3.3.0\ncp <repo>/packages/bench/typia-case/src/index.ts src/index.ts\ncp <repo>/packages/bench/typia-case/tsconfig.json tsconfig.json\nnub exec --node ts-patch install && nub exec --node tsc -p tsconfig.json\ncp build/index.js <repo>/packages/bench/typia-case/build/index.cjs\n```\n","isInternal":false,"tokens":273,"sizeBytes":1024},{"name":"README.md","path":"packages/docs/README.md","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/packages/docs/README.md","title":"docs Documentation","category":"plugin-manifest","format":"markdown","content":"# onepager-fuma\n\n<Tabs groupId=\"lib\" items={[\"Zod\", \"Zod Mini\"]}>\n<Tab value=\"Zod\">\n</Tab>\n<Tab value=\"Zod Mini\">\n</Tab>\n</Tabs>\n","isInternal":false,"tokens":49,"sizeBytes":129},{"name":"README.md","path":"packages/mini/README.md","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/packages/mini/README.md","title":"mini Documentation","category":"plugin-manifest","format":"markdown","content":"# @zod/mini\n\nZod Mini as a standalone package. It re-exports [`zod/mini`](https://zod.dev/packages/mini) from a peer dependency on `zod`, so every schema it creates is a `zod` schema.\n\n```sh\nnpm install zod @zod/mini\n```\n\n```ts\nimport * as z from \"@zod/mini\";\n\nconst User = z.object({\n  name: z.string(),\n  age: z.number().check(z.int(), z.positive()),\n});\n\nz.parse(User, { name: \"Colin\", age: 30 });\n```\n\nThe version tracks `zod`: each `@zod/mini@4.x.y` requires `zod@^4.x.0`.\n","isInternal":false,"tokens":160,"sizeBytes":478},{"name":"README.md","path":"packages/resolution/README.md","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/packages/resolution/README.md","title":"resolution Documentation","category":"plugin-manifest","format":"markdown","content":"# Package resolution checks\n\nRun from the repository root:\n\n```sh\nnub run build\nnub run --filter @zod/resolution test:all\n```\n\nThese checks inspect the published declarations with Are the Types Wrong and execute the built CommonJS and ES modules on plain Node through Nub.\n","isInternal":false,"tokens":62,"sizeBytes":273},{"name":"README.md","path":"packages/tsc/README.md","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/packages/tsc/README.md","title":"tsc Documentation","category":"plugin-manifest","format":"markdown","content":"# Zod perftesting\n\nRun `nub run generate` to generate random Zod schemas.\n\nRun `nub run build:bench` to run `tsc` with `extendedDiagnostics`.\n\nThe workspace dependency points to the local Zod package. Rebuild Zod after changing its typings.\n","isInternal":false,"tokens":62,"sizeBytes":241},{"name":"README.md","path":"packages/zod/README.md","rawUrl":"https://raw.githubusercontent.com/colinhacks/zod/HEAD/packages/zod/README.md","title":"zod Documentation","category":"plugin-manifest","format":"markdown","content":"<p align=\"center\">\n  <img src=\"logo.svg\" width=\"200px\" align=\"center\" alt=\"Zod logo\" />\n  <h1 align=\"center\">Zod</h1>\n  <p align=\"center\">\n    TypeScript-first schema validation with static type inference\n    <br/>\n    by <a href=\"https://x.com/colinhacks\">@colinhacks</a>\n  </p>\n</p>\n<br/>\n\n<p align=\"center\">\n<a href=\"https://github.com/colinhacks/zod/actions?query=branch%3Amain\"><img src=\"https://github.com/colinhacks/zod/actions/workflows/test.yml/badge.svg?event=push&branch=main\" alt=\"Zod CI status\" /></a>\n<a href=\"https://opensource.org/licenses/MIT\" rel=\"nofollow\"><img src=\"https://img.shields.io/github/license/colinhacks/zod\" alt=\"License\"></a>\n<a href=\"https://www.npmjs.com/package/zod\" rel=\"nofollow\"><img src=\"https://img.shields.io/npm/dw/zod.svg\" alt=\"npm\"></a>\n<a href=\"https://discord.gg/KaSRdyX2vc\" rel=\"nofollow\"><img src=\"https://img.shields.io/discord/893487829802418277?label=Discord&logo=discord&logoColor=white\" alt=\"discord server\"></a>\n<a href=\"https://github.com/colinhacks/zod\" rel=\"nofollow\"><img src=\"https://img.shields.io/github/stars/colinhacks/zod\" alt=\"stars\"></a>\n</p>\n\n<div align=\"center\">\n  <a href=\"https://zod.dev/api\">Docs</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://discord.gg/RcG33DQJdf\">Discord</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://twitter.com/colinhacks\">𝕏</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://bsky.app/profile/zod.dev\">Bluesky</a>\n  <br />\n</div>\n\n<br/>\n<br/>\n\n### [Read the docs →](https://zod.dev/api)\n\n<br/>\n<br/>\n\n## What is Zod?\n\nZod is a TypeScript-first validation library. Define a schema and parse some data with it. You'll get back a strongly typed, validated result.\n\n```ts\nimport * as z from \"zod\";\n\nconst User = z.object({\n  name: z.string(),\n});\n\n// some untrusted data...\nconst input = {\n  /* stuff */\n};\n\n// the parsed result is validated and type safe!\nconst data = User.parse(input);\n\n// so you can use it with confidence :)\nconsole.log(data.name);\n```\n\n<br/>\n\n## Features\n\n- Zero external dependencies\n- Works in Node.js and all modern browsers\n- Tiny: `2kb` core bundle (gzipped)\n- Immutable API: methods return a new instance\n- Concise interface\n- Works with TypeScript and plain JS\n- Built-in JSON Schema conversion\n- Extensive ecosystem\n\n<br/>\n\n## Installation\n\n```sh\nnpm install zod\n```\n\n<br/>\n\n## Basic usage\n\nBefore you can do anything else, you need to define a schema. For the purposes of this guide, we'll use a simple object schema.\n\n```ts\nimport * as z from \"zod\";\n\nconst Player = z.object({\n  username: z.string(),\n  xp: z.number(),\n});\n```\n\n### Parsing data\n\nGiven any Zod schema, use `.parse` to validate an input. If it's valid, Zod returns a strongly-typed _deep clone_ of the input.\n\n```ts\nPlayer.parse({ username: \"billie\", xp: 100 });\n// => returns { username: \"billie\", xp: 100 }\n```\n\n**Note** — If your schema uses certain asynchronous APIs like `async` [refinements](https://zod.dev/api#refinements) or [transforms](https://zod.dev/api#transforms), you'll need to use the `.parseAsync()` method instead.\n\n```ts\nconst schema = z.string().refine(async (val) => val.length <= 8);\n\nawait schema.parseAsync(\"hello\");\n// => \"hello\"\n```\n\n### AOT compilation\n\nFor hot validation paths, `z.compile(schema)` returns a schema clone with an ahead-of-time compiled fast path. Valid inputs take the compiled path; invalid inputs fall back to the regular parser so error reporting stays identical.\n\nAcross a 55-schema benchmark the median speedup is **2.4x**, and it scales with how much work the schema does per parse: a large array of objects is ~9x, a 20-key object ~9x, a nested object ~4.5x, while a bare `z.string()` gains nothing — compilation removes per-node dispatch and allocation, and a single `typeof` has none to remove.\n\n```ts\nconst CompiledPlayer = z.compile(Player);\n\nCompiledPlayer.parse({ username: \"billie\", xp: 100 });\n```\n\nTo enable compilation globally for schemas constructed after import:\n\n```ts\nimport \"zod/compile\"; // place before modules that define schemas\n```\n\nThings to know:\n\n- Compilation uses `new Function`. Global mode is automatically disabled when `z.config({ jitless: true })` is set (e.g. CSP environments); calling `z.compile()` directly is an explicit opt-in.\n- Schemas with async refinements or transforms can't be compiled, and neither can a few other constructs. That is not an error: `z.compile()` hands the schema back unchanged and it keeps using the regular parser, exactly as global mode leaves it. Pass `{ strict: true }` to throw `ZodCompileAsyncError` / `ZodCompileUnsupportedError` instead.\n- On invalid input, refinements and transforms may run twice (fast path, then fallback).\n- Deriving a new schema from a compiled one (`.refine()`, `.extend()`, etc.) returns an uncompiled schema — compile the final schema.\n\nSee [`compile` docs](https://zod.dev/compile) for details.\n\n### Handling errors\n\nWhen validation fails, the `.parse()` method will throw a `ZodError` instance with granular information about the validation issues.\n\n```ts\ntry {\n  Player.parse({ username: 42, xp: \"100\" });\n} catch (err) {\n  if (err instanceof z.ZodError) {\n    err.issues;\n    /* [\n      {\n        expected: 'string',\n        code: 'invalid_type',\n        path: [ 'username' ],\n        message: 'Invalid input: expected string, received number'\n      },\n      {\n        expected: 'number',\n        code: 'invalid_type',\n        path: [ 'xp' ],\n        message: 'Invalid input: expected number, received string'\n      }\n    ] */\n  }\n}\n```\n\nTo avoid a `try/catch` block, you can use the `.safeParse()` method to get back a plain result object containing either the successfully parsed data or a `ZodError`. The result type is a [discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions), so you can handle both cases conveniently.\n\n```ts\nconst result = Player.safeParse({ username: 42, xp: \"100\" });\nif (!result.success) {\n  result.error; // ZodError instance\n} else {\n  result.data; // { username: string; xp: number }\n}\n```\n\n**Note** — If your schema uses certain asynchronous APIs like `async` [refinements](https://zod.dev/api#refinements) or [transforms](https://zod.dev/api#transforms), you'll need to use the `.safeParseAsync()` method instead.\n\n```ts\nconst schema = z.string().refine(async (val) => val.length <= 8);\n\nawait schema.safeParseAsync(\"hello\");\n// => { success: true; data: \"hello\" }\n```\n\n### Inferring types\n\nZod infers a static type from your schema definitions. You can extract this type with the `z.infer<>` utility and use it however you like.\n\n```ts\nconst Player = z.object({\n  username: z.string(),\n  xp: z.number(),\n});\n\n// extract the inferred type\ntype Player = z.infer<typeof Player>;\n\n// use it in your code\nconst player: Player = { username: \"billie\", xp: 100 };\n```\n\nIn some cases, the input & output types of a schema can diverge. For instance, the `.transform()` API can convert the input from one type to another. In these cases, you can extract the input and output types independently:\n\n```ts\nconst mySchema = z.string().transform((val) => val.length);\n\ntype MySchemaIn = z.input<typeof mySchema>;\n// => string\n\ntype MySchemaOut = z.output<typeof mySchema>; // equivalent to z.infer<typeof mySchema>\n// number\n```\n","isInternal":false,"tokens":1932,"sizeBytes":7304},{"name":".cursorrules","path":".cursorrules (typescript-zod-tailwind-nextjs-cursorrules-prompt-.mdc)","rawUrl":"https://raw.githubusercontent.com/PatrickJS/awesome-cursorrules/main/rules/typescript-zod-tailwind-nextjs-cursorrules-prompt-.mdc","title":"Awesome Cursorrules: typescript zod tailwind nextjs cursorrules prompt ","category":"cursor-rule","format":"markdown","content":"---\ndescription: \"Cursor rules for TypeScript development with Zod, Tailwind, and Next.js integration.\"\nglobs: **/*\nalwaysApply: false\n---\n# Coding Style Guide\n\nCode Style and Structure:\n- Write concise, technical TypeScript code with accurate examples\n- Use functional and declarative programming patterns; avoid classes\n- Prefer iteration and modularization over code duplication\n- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError)\n- Structure files: exported component, subcomponents, helpers, static content, types\n\nNaming Conventions:\n- Use lowercase with dashes for directories (e.g., components/auth-wizard)\n- Favor named exports for components\n\nTypeScript Usage:\n- Use TypeScript for all code; prefer interfaces over types\n- Avoid enums; use maps instead\n- Use functional components with TypeScript interfaces\n- Use Zod for form validation\n\nSyntax and Formatting:\n- Use the \"function\" keyword for pure functions\n- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements\n- Use declarative JSX\n\nError Handling and Validation:\n- Prioritize error handling: handle errors and edge cases early\n- Use early returns and guard clauses\n- Implement proper error logging and user-friendly messages\n- Use Zod for form validation\n- Model expected errors as return values in Server Actions\n- Use error boundaries for unexpected errors\n\nUI and Styling:\n- Use Shadcn UI, Radix, and Tailwind Aria for components and styling\n- Implement responsive design with Tailwind CSS; use a desktop-first approach\n\nPerformance Optimization:\n- Minimize 'useEffect', and 'setState'; favor React Remix Components (RSC)\n- Wrap client components in Suspense with fallback\n- Use dynamic loading for non-critical components\n- Optimize images: use WebP format, include size data, implement lazy loading\n\nKey Conventions:\n- Use proper URL search parameter state management\n- Optimize Web Vitals (LCP, CLS, FID)\n- Limit 'use client'\n\nWhen React Server Components (RSC) are used:\n- Favor server components and Next.js SSR\n- Use only for Web API access in small components\n- Avoid for data fetching or state management\n\nFollow React Remix docs for Data Fetching, Rendering, and Routing\n\nFollow Next.js docs for Data Fetching, Rendering, and Routing when Next JS is used instead of React Remix\n","tokens":488,"sizeBytes":2322}],"systemPromptSnippet":"<yakaai_skills repo=\"colinhacks/zod\">\n<!-- File: AGENTS.md (Tokens: ~5171 | Category: root-instruction) -->\n# AGENTS.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Development Commands\n\nUse Nub exclusively for dependency management, scripts, and TypeScript execution. The root `package.json` declares the workspaces and pins Nub; `.nvmrc` selects Node. Nub reads and writes the existing `pnpm-lock.yaml` without requiring pnpm.\n\nRun scripts with `nub run`, repository TypeScript with `nub <file>`, and installed tools with `nub exec --node <tool>`. Keep third-party tools and built-package compatibility probes on plain Node; Nub's augmentation must not mask resolution errors or change test behavior. Use `nub install --frozen-lockfile` for reproducible installs and `nub add` / `nub remove` to manage dependencies.\n\nKey commands:\n\n- `nub run build` - Build all packages (runs recursive build command)\n- `nub exec --node vitest run` - Run all tests with Vitest. Includes the compile-mode project, which re-runs the zod tests with global AOT compilation enabled (see `wiki/compile.md`).\n- `nub exec --node vitest run <path>` - Run specific test file (e.g., `packages/zod/src/v4/classic/tests/string.test.ts`)\n- `nub exec --node vitest run <path> -t \"<pattern>\"` - Run specific test(s) within a file (e.g., `-t \"MAC\"`)\n- `nub exec --node vitest run --update` - Update all test snapshots\n- `nub exec --node vitest run <path> --update` - Update snapshots for specific test file\n- `nub run test:watch` - Run tests in watch mode\n- `nub exec --node vitest run --coverage` - Run tests with coverage report\n- `nub run test:compile` - Focused alias for just the compile-mode project. Already covered by `nub run test`; use this when iterating on compile-related changes.\n- `nub run dev` - Execute TypeScript with Nub under source conditions\n- `nub run dev <file>` - Execute `<file>` with Nub & proper resolution conditions. Usually use for `play.ts`.\n- `nub run dev:play` - Quick alias to run play.ts for experimentation\n- `nub run check:comments` - Fail on stacked `//` comment lines (`--fix` joins them)\n- `nub run lint` - Run biome linter with auto-fix\n- `nub run format` - Format code with biome\n- `nub run fix` - Run both format and lint\n\n## Rules\n\n- Nub v0.8.3 and Node.js v24+ required; Nub provisions Node from `.nvmrc`\n- ES modules are used throughout (`\"type\": \"module\"`)\n- All tests must be written in TypeScript - never use JavaScript\n- Use `play.ts` for quick experimentation; use proper tests for all permanent test cases\n- Features without tests are incomplete - every new feature or bug fix needs test coverage\n- Don't skip tests due to type issues - fix the types instead\n- Test both success and failure cases with edge cases\n- Keep added tests as minimal and dense as possible without sacrificing comprehensiveness; avoid redundant assertions or broad fixtures when a focused case proves the behavior.\n- No log statements (`console.log`, `debugger`) in tests or production code\n- Never stack prose across consecutive `//` lines. Lines have no maximum width here — the editor wraps for display — so a paragraph split across several `//` lines is just a hard-wrapped line, and hard wrapping breaks search, diffs and editing. Write one long `//` instead. `nub run check:comments` enforces this in pre-commit and CI; `--fix` joins the offenders. Commented-out code, `@ts-`/`@__NO_SIDE_EFFECTS__`-style pragmas, bullet lists, and blocks separated by a bare `//` are exempt. When two adjacent comments describe two different statements, separate them with a blank line rather than joining them.\n- Keep comments SHORT AND TIGHT — one lowercase sentence fragment, one clause, no trailing period. Never a capitalized full sentence, and never two of them. Extreme concision: say only what the code cannot say, and cut the setup sentence, the recap, and the same point restated in different words; a comment that needs three sentences usually means the code should be clearer. Identifiers keep their real casing (`Error`, `parse()`), only the prose is lowercase.\n- Ask before generating new files\n- Use `util.defineLazy()` for computed properties to avoid circular dependencies\n- Never branch on specific schema types in shared code. No `def.type === \"optional\"` conditionals, no hardcoded lists of wrapper type names, no walks up the wrapper chain hunting for a particular type. Every schema type added later silently falls through such a check, and the list is wrong the moment someone writes a new wrapper. When a shared path needs to know something about a schema, express it as a structural property on the internals — `optin`/`optout`, `values`, `pattern`, `propValues` — and let each type declare its own answer. This is not negotiable in the parse paths; a PR that adds edge-case conditional logic keyed on schema types will be rejected regardless of how well it is tested.\n- Performance is critical - parameter reassignment is allowed for optimization\n- Any change to `packages/zod/src` must be weighed on **all three axes: runtime performance, memory consumption, and bundle size** — see \"The three axes\" below. A change that improves one and is only checked on that one is not finished.\n- ALWAYS use the `gh` CLI to fetch GitHub information (issues, PRs, etc.) instead of relying on web search or assumptions\n- Keep JSDoc as minimal as possible. A self-explanatory type or symbol name needs no doc comment. When a comment is genuinely required, write one short sentence describing behavior — not history, rationale, or examples. Don't add interface-level JSDoc that just restates the interface name.\n- When you've modified a PR (or opened/closed/commented on one), include the PR URL liberally in summary messages — at minimum once at the end of any reply that touched it\n- When creating a PR, do not include a separate test plan section in the body. Link to any relevant issues under discussion, and use the same copywriting guidelines from \"Commenting on issues and PRs\": concise maintainer voice, prose over templates, and validation details only when they are material to the reader.\n- Format validators (`z.iso.*`, `z.email()`, `z.url()`, `z.uuid()`, …) are deliberately narrower than the specs they're named after. \"The spec allows X\" is not a reason to accept X — see \"Format validators: spec compliance is not the bar\" below.\n- NEVER bump the version in `packages/zod/package.json` (or any package's `package.json`), and NEVER run `gh workflow run release.yml`. Publishing is a maintainer-only dispatch behind an approval. If a version bump is genuinely needed, ask first.\n\n## The three axes\n\nZod is judged on **runtime performance**, **memory consumption**, and **bundle size** at once, and they trade against each other constantly. Optimizing one in isolation is how regressions land: a change that buys bundle bytes can cost construction speed, and one that saves memory can cost both. Any non-trivial change to `packages/zod/src` — and any change to `core/` at all, since every build ships it — needs a number on all three before it is done. Report them together, including the ones that got worse.\n\n`zod/mini` deserves its own line on the bundle axis: it is sold on size, so a fixed cost lands very differently there (~200 B is 6% of the smallest mini bundle and 1% of classic's).\n\n| axis    | how to measure                                                                                                                                    |\n| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| runtime | `packages/bench/*.ts` via `nub run bench <name>`, plus construction cost — parse and construction move independently                                 |\n| memory  | `packages/bench/memory/schema-footprint.ts` and `realworld.ts` (retained bytes per schema; run under `--expose-gc`)                               |\n| bundle  | bundle a `packages/treeshake` fixture with esbuild `--minify` under `--conditions=@zod/source` and gzip it, for both a classic and a mini fixture |\n\n### Benchmarking traps\n\nThese produced hours of wrong conclusions; check them before believing a number.\n\n- **Check `uptime` first.** A loaded machine invents effects of ±16%. Numbers taken above a load average of ~8 are worthless.\n- **Interleave the two revisions.** Running all of A then all of B puts any drift entirely on whichever ran second. Alternating round-by-round took per-side spread from 2.5x to 4%.\n- **Loading two zod revisions into one process makes call sites polymorphic.** It is fine for comparing construction, unreliable for tight leaf parses.\n- **Allocating benchmarks can't be timed by a fixed-duration loop.** `z.array().parse()` allocates per call, so a time-boxed harness samples whatever the collector is doing — it gave +8.2% and −17.3% on consecutive identical runs. Use a fixed iteration count with `gc()` between samples and take the minimum.\n- **Make sure the work isn't optimized away.** If a micro-benchmark reports ~1e9 ops/sec, V8 eliminated the loop; consume the result.\n\n### Property placement, specifically\n\nMost of the memory in a schema is its own-property count, not its closures. V8 sizes an instance's backing store in steps and `$constructor` assigns nothing itself, so instances get no in-object slots: **≤12 own properties cost 128 bytes, 13–20 cost 848, ≥21 cost 1616** (`packages/bench/memory/prop-slack.ts` measures this). Members therefore live on the prototype and materialize per instance on first read.\n\nIf you touch that machinery, three things bite:\n\n- **Moving a property between definition sites changes its descriptor**, and writability/enumerability/configurability are part of the public contract. `packages/bench/memory/` has a surface-diff approach for this: dump every descriptor, alias and assign/delete semantic on both revisions and diff them. It caught four real regressions that tests did not.\n- **Redefining an accessor demotes the object to dictionary mode**, which cost 2x on `z.object().parse` once. Run `packages/bench/memory/dict-mode.ts` after any change here — every instance must report `fast`.\n- **A bound function pays a call-time trampoline.** Fine for cold builder methods, measurably not fine on the parse path.\n\n## Format validators: spec compliance is not the bar\n\nZod's format validators — `z.iso.*`, `z.email()`, `z.url()`, `z.uuid()`, and the rest — are named after specs but do not implement them. Each one matches the profile that real producers emit and real consumers accept, which is almost always far narrower than what the grammar permits. That narrowness is the product, not a gap in it.\n\nSo **\"the spec allows X, therefore Zod must accept X\" is not an argument**, and a PR that widens a format regex on that basis alone will be closed. The question is never what the spec permits; it's whether accepting the wider form helps more people than rejecting it hurts. A form that shows up in real payloads and breaks real integrations is worth fixing. A form that only appears when someone reads the grammar and constructs a string from it is not — when one of those reaches a validator in production it's a bug or an attack, and rejecting it is the useful behavior.\n\nThree things make a widening request an automatic no:\n\n- **The spec permits the form only \"by mutual agreement\" of the two parties.** A general-purpose validator is never party to that agreement, so it can't honor the clause. ISO 8601 expanded years (`+010000-01-01T00:00:00.000Z`) are the canonical case: the standard also requires the parties to agree the digit count, and runtimes disagree — JavaScript emits six digits, Java's `Instant` emits five, Python rejects both. Declined in #6154.\n- **The wider form breaks the JSON Schema output.** `z.iso.datetime()` and `z.iso.date()` emit `format: \"date-time\"` and `format: \"date\"`, which mean RFC 3339 — four-digit years, no sign. Widening those regexes makes `z.toJSONSchema()` emit a pattern that conformant consumers reject.\n- **The cost lands on everyone.** These regexes live in `core/regexes.ts`, so every added alternation is bytes in every bundle including `zod/mini`, plus more backtracking on a hot parse path. Weigh it on all three axes above before you argue for it at all.\n\nFor calibration on how narrow these already are: `z.iso.datetime()` rejects basic format (`20200101T061500Z`), week dates (`2020-W01-1`), ordinal dates (`2020-001`), comma decimal separators, and `24:00`. Every one of those is valid ISO 8601. None of them are going in either.\n\nPoint anyone who genuinely needs a wider format at `z.string().regex()` or `z.string().refine()`. That escape hatch is the answer, not a wider default.\n\n## Triaging issues and PRs\n\nFollow the `triage` skill at [`.claude/skills/triage/SKILL.md`](.claude/skills/triage/SKILL.md) whenever you're asked to investigate, triage, or form an opinion on an issue or PR. It is the single source of truth for the procedure. Claude and Codex both auto-discover it (Codex via the `.codex/skills` symlink); read it directly if your agent doesn't.\n\nFor a draft **security advisory** — a GHSA id, the Security tab, a private vulnerability report — use the `security-advisory` skill at [`.claude/skills/security-advisory/SKILL.md`](.claude/skills/security-advisory/SKILL.md) instead. It shares the conventions above but reorders the work: the fix lands on `main` before any reporter comment is drafted, since the comment's whole value is the PR it links to.\n\nWrite-ups live in the gitignored `.triage/` tree — one directory per ticket, `.triage/issues/NNNN/results.md` and `.triage/prs/NNNN/results.md`, with scratch files and repros alongside. When investigating a PR from its worktree, resolve the root repo first (`git worktree list | head -1`) and write results back there; a relative write lands in the worktree's own ignored `.triage/` where nobody will find it.\n\n## Iterating on a contributor PR in a worktree\n\nWhen asked to make changes on top of an open PR (e.g. as a maintainer review suggestion), use a worktree so `main` stays clean:\n\n```bash\n# 1. Fetch the PR as a local branch and create a worktree for it\ngit fetch origin pull/<N>/head:pr-<N>\ngit worktree add ~/.cursor/worktrees/zod/pr-<N> pr-<N>\ncd ~/.cursor/worktrees/zod/pr-<N>\nnub install --frozen-lockfile   # fast, Nub's store is shared across worktrees\n\n# 2. Look up the PR's head info — you'll need the contributor's fork URL\n#    and the head ref name to push back.\ngh pr view <N> --repo colinhacks/zod \\\n  --json headRefName,headRepositoryOwner,maintainerCanModify\n\n# 3. If maintainerCanModify is true, add the fork as a remote and push to\n#    the PR's head ref (NOT to your local branch name).\ngit remote add <contributor> git@github.com:<contributor>/zod.git\ngit push <contributor> pr-<N>:<headRefName>                 # first push\ngit push <contributor> pr-<N>:<headRefName> --force-with-lease   # for amends\n```\n\nNotes:\n\n- Do NOT use `gh pr checkout --detach` for this — it moves your _current_ working tree into detached HEAD instead of creating a worktree.\n- Husky pre-commit runs biome format/lint via lint-staged; pre-push runs the full vitest suite. Both are fast and act as a safety net — don't bypass with `--no-verify` unless you have a specific reason.\n- **Preserve contributor commits.** Never `git reset --hard` or otherwise rewrite history that erases the contributor's work, even if you're rewriting the actual change. They need to stay in the PR's commit list to get credit on the merged PR. If their approach was wrong, add a `Revert \"...\"` commit (or just a plain commit that undoes those lines) and then add your replacement commit on top. Force-pushing a single clobbering commit strips them from the GitHub contributors graph.\n- When done, clean up: `git worktree remove ~/.cursor/worktrees/zod/pr-<N>` and `git branch -D pr-<N>` (and optionally `git remote remove <contributor>`).\n\n## Commenting on issues and PRs\n\nWhen posting on a maintainer's behalf via `gh` (PR comments, issue comments, reviews), match the house tone. Load the `prose-writing` skill before you write any of it — it is the general copy guide, and this section layers on top of it. The register is authoritative and friendly — concise, not bubbly, not over-explaining, not effusive. Comments come from a maintainer handing down decisions, not negotiating them. Friendly does not mean deferential.\n\n- Exclamation points are fine in moderation, especially to soften a decline or close out a thread (\"Thanks for looking into this!\"). Don't stack them and don't sprinkle them through technical writeups.\n- Skip effusive praise: \"Great work\", \"Awesome\", \"Thanks so much for this\", \"Thanks for the careful writeup\", \"you clearly read the RFC\". A short flat-affect affirmation walks the line well — \"Good investigation.\" or \"Solid catch.\" with a period, no exclamation, no superlatives. Warmth otherwise comes from a short closer (\"Thanks for looking into this, though 👍\"), not a preamble that butters up the contributor before the decision.\n- No \"PTAL\", \"WDYT\", or sign-off flourishes asking the contributor to re-review changes the maintainer pushed on top. State what changed and the merge intent. (\"LGTM\" is fine as a literal verdict at the end of a substantive review, not as a sign-off.)\n- When the user gives you exact wording for a comment, use it verbatim (fixing only obvious typos). Do not \"improve\" their phrasing to match this style guide — their direct instruction wins.\n- Lead with the decision or action: \"Going to merge as-is.\" \"Closing this out.\" \"I'd be open to a top-level utility but not as a method.\" Then the reasoning.\n- First person, owned opinions. \"I don't think this should be a method.\" \"I'm hesitant to add this.\" Don't hide behind passive voice or \"we could perhaps consider\".\n- Speak with authority. No hedging (\"maybe\", \"I think perhaps\", \"if that's okay\"), no apologizing for decisions, no asking permission to land changes the maintainer has already made. Decisions are stated as decisions.\n- Be direct when declining, but not curt. \"out of scope\", \"behaving as intended\", \"this is more complicated than it looks\" — firm, with a concrete reason. A friendly closer (\"thanks for looking into this\") is fine.\n- Cross-reference by number: `#4433`, `commit 2f8414bc`, `merged in #5718`. Concrete and verifiable.\n- Length matches substance, and substance is almost never as much as you think. Two or three sentences closes most threads. How long you spent investigating has nothing to do with how long the reply should be — a week of benchmarking still closes in three sentences. The `triage` skill works this through against a real decline.\n- No file paths, line numbers, or symbol names unless the reader has to open that file. They turn a two-line decision into a report, and nobody asked you to show your work. Say what the code does instead — \"the parse context is per-call state now\", not \"`core/memoizer.ts:133` writes onto the context\". The detail belongs in the `.triage/` write-up.\n- Pick the one or two strongest reasons and write them as prose. Resist enumerating every objection in a bullet list — even when each point is fair, it reads as piling on. The strongest argument plus a concrete escape hatch (e.g. \"`z.email().max(254)` already does this\") is usually enough.\n- Don't lift informal or coarse phrasing from external sources (blog posts, issues, comments) into the maintainer voice, even in quotes. Paraphrase the substance — quoted-in-context still reads as the maintainer talking.\n- Use prose with inline backticks for symbols. Reach for fenced code blocks only when showing non-trivial code is genuinely clearer than describing it.\n- Skip emojis in substantive technical writeups. A small `👍` in a casual closer is good — it keeps a decline or sign-off sounding warm and friendly without leaning on praise.\n- Bot mentions are bare imperatives: `@pullfrog review`, `@pullfrog fix merge conflicts`, `@pullfrog re-review fresh.`\n- When pushing a follow-up on top of a contributor's PR, state what changed, why it differs from the original approach, and that the maintainer is merging. Never ask the contributor to review the maintainer's changes — they are final, not a proposal. Don't thank them for \"letting\" the maintainer rewrite their work.\n- When posting comments with code samples via `gh`, do NOT pass the body inline through a heredoc that requires escaping backticks. Backslash-escaped backticks (`` \\` ``) inside a `$(cat <<'EOF' ... EOF)` body get sent to GitHub literally and break inline code and template literals inside fenced blocks. Instead, write the comment to a file and pass it via `--body-file <path>` (for `gh pr/issue comment`) or `-F body=@<path>` (for `gh api`). This preserves backticks and `${...}` exactly as written.\n\n## Pushing to a new branch (don't accidentally push to main)\n\nWhen the user asks for a \"new PR\" or \"new branch\", the work has to land on a non-`main` ref on the remote. The footgun: `git worktree add <path> -b <branch> origin/main` (and `git checkout -b <branch> origin/main`) silently set the new branch's upstream to `refs/heads/main` because the start point is a remote-tracking ref. A subsequent `git push -u origin <branch>` then pushes to `refs/heads/main`, not to a new remote branch. Two of these in a row have happened.\n\nAvoid by being explicit on the first push:\n\n```bash\n# Always use a refspec on the initial push so the remote ref name is unambiguous.\ngit push -u origin <branch>:refs/heads/<branch>\n```\n\nThen verify the output. The line you want to see is:\n\n```\n* [new branch]      <branch> -> <branch>\n```\n\nIf the right-hand side says `main`, the push went to main — abort or revert.\n\nUse `git push origin HEAD:refs/heads/<branch>` for subsequent pushes if you didn't `-u` originally. Don't rely on `git push -u origin <branch>` alone — its behavior depends on the upstream config, which `worktree add -b ... origin/main` set wrong for you.\n\n<!-- File: CLAUDE.md (Tokens: ~3 | Category: claude-rule) -->\nAGENTS.md\n\n<!-- File: .claude/skills/security-advisory/SKILL.md (Tokens: ~2222 | Category: anthropic-skill) -->\n---\nname: security-advisory\ndescription: Triage a draft security advisory in colinhacks/zod — a private vulnerability report from the Security tab, named by GHSA id (GHSA-xxxx-xxxx-xxxx) or reached from \"the draft advisories\", \"the security reports\", \"the vulnerability queue\". Use instead of the `triage` skill whenever the ticket is an advisory rather than a public issue or PR, because the workflow is different: the report is private, the burden of proof is on the reporter, the fix must be LANDED before any comment is drafted, and the publish-or-decline call has its own calculus. Covers reproducing a PoC against main, the schema-vs-input test, deduplicating a cluster of reports into distinct defects, landing the fix first, the publish-vs-decline framework, per-reporter privacy rules that make a GHSA id unlinkable, and the results.md format under .triage/advisories/GHSA-xxxx-xxxx-xxxx/.\n---\n\n# Triaging a draft security advisory\n\nAdvisories are not issues with a scarier label. Four things differ, and each one changes the order you work in:\n\n- **The report is private**, and private in both directions — see `## Privacy cuts both ways`.\n- **The burden of proof is on the reporter.** Most submissions are scanner output or a rediscovery of intended behavior. A plausible writeup is not evidence.\n- **The fix lands before the comment exists.** See `## Land the fix first`. This is the rule most often broken and the one that wastes the most of Colin's time.\n- **Real defect and publishable vulnerability are different questions.** See `## Publish or decline`.\n\nEverything in the `triage` skill about where files live, `results.md` frontmatter, close-comment voice, and reporting back in chat still applies. This skill replaces only its `## Investigating a draft advisory` section.\n\n**Never accept, publish, decline, or comment on an advisory.** The write-up and the drafted note are the deliverables; the disposition is Colin's. Landing a fix is a code change and follows the repo's normal rules; publishing a CVE is not, and is never yours.\n\n## Reading the queue\n\n```bash\ngh api repos/colinhacks/zod/security-advisories --paginate \\\n  --jq '.[] | select(.state==\"triage\") | [.ghsa_id, .severity, .summary] | @tsv'\ngh api repos/colinhacks/zod/security-advisories/<GHSA-ID> --jq '.description'   # full report + PoC\n```\n\nWrite-ups go to `.triage/advisories/<GHSA-ID>/results.md`, scratch files beside them. Frontmatter swaps `number:` for `ghsa:` and adds `severity_claimed:` (what the reporter asserted) next to `severity_actual:` (yours, or `none`).\n\n`verdict` is one of `valid`, `not-a-vulnerability`, `already-fixed`, `duplicate` (with `duplicate_of:`), `needs-reporter-info`, `needs-maintainer-decision`. Add `fixed_by:` / `fix_pr:` once a fix lands, and `disposition_recommended:` when you have a publish-or-decline opinion. Keep `verdict` describing the defect and `disposition_recommended` describing what to do about it — do not let a decline quietly rewrite a real finding as `not-a-vulnerability`.\n\n## Order of work\n\n1. **Extract the PoC verbatim** into `.triage/advisories/<GHSA-ID>/poc.ts`. Do not clean it up first — a PoC that only works after you fix it is a finding in itself.\n2. **Reproduce against current `main`**: `nub run dev .triage/advisories/<GHSA-ID>/poc.ts`. If it does not reproduce, say so and check whether it ever did (`git log -S`, or test the version the reporter named).\n3. **Decide whether the reproduced behavior is a vulnerability at all.** The recurring question on this repo is whether the attacker controls the **schema** or only the **input**. A schema is application code, so a report needing an attacker-authored schema is usually `not-a-vulnerability`. For prototype reports: is the polluted prototype the returned object's own (contained) or `Object.prototype` itself (real)?\n4. **Enumerate reachability yourself; do not inherit the reporter's.** Reporters test the path they found. Grep every call site that can feed attacker data into the sink and build a route table — precondition per route, and whether each needs only input, or a particular schema shape, or cooperating application code. This is where the severity actually gets decided, and it routinely turns up both routes the reporter missed and routes that are already closed.\n5. **Dedupe the cluster.** Many reports describe one defect. `grep -rl '<keyword>' .triage/advisories/*/results.md`, pick a canonical, cross-link the rest with `duplicate_of`. Fold by underlying defect, never by title keyword.\n6. **Check for public prior art** — an open issue or PR describing the same bug without the security framing. If one exists the defect is already public, which kills the embargo and changes the disposition.\n7. **Land the fix** — see below.\n8. **Then** write `results.md` and draft the reporter note.\n\n## Land the fix first\n\nA close comment's entire value is the line that says where it was fixed. Draft one before a fix exists and you have written a decline, whatever it says — the reporter gets no commit to verify, no release to wait for, and no reason to believe the report was acted on. Colin then has to do the work you were asked to do before he can send anything.\n\nSo the fix is not a follow-up to the triage. It is a step inside it, and it comes before any comment is drafted:\n\n- **Search for existing PRs before writing one.** Popular defects attract near-duplicate community PRs — this repo had seven for a single formatter bug. Read every diff, rank them, and take the best as a base rather than opening an eighth.\n- **Judge the candidates against the whole defect, not the reported symptom.** A patch that fixes the one function in the PoC while three sibling call sites keep the bug is partial. Build a behavior matrix of the cases that matter, run it against `main` and against each candidate patch, and let that table pick the winner. This is also how you catch a patch that trades a crash for silent data loss.\n- **Extend the base rather than starting over**, and preserve the contributor's commits — AGENTS.md covers pushing to a contributor's head ref.\n- **Run what CI runs** before pushing, then confirm CI itself is green.\n- **Do not bump a version and do not cut a release.** Landing on `main` is the goal. Release timing on a security fix is Colin's, and on this repo approving the release workflow's `npm` environment is the one irreversible action.\n\nOnly once the fix is merged do you write the comment, and the comment names the merge commit and the PR.\n\nIf the fix cannot land — the right change is a breaking one, or the affected major is EOL — say so explicitly in `results.md` and draft the comment around what the reporter's actual remedy is. That is a real outcome, not a skipped step.\n\n## Privacy cuts both ways\n\nThe report body may contain an unpatched exploit, so it never goes into a public issue, PR, or commit message. A fix can land publicly while the advisory stays private — describe the defect neutrally in the commit and let the mechanism speak for itself.\n\nThe direction that gets missed: **each reporter can see only their own advisory thread.** A GHSA id in a comment is a dead link to everyone but the maintainers. A duplicate close must never say \"duplicate of GHSA-xxxx\" or \"see the reasoning on that thread\" — the reporter can open neither. Every comment stands alone: state the reasoning inline, and refer to a sibling report as \"a separate report of the same defect\", without the id and without the other reporter's name. Public issues, PRs and commit SHAs are the only cross-references a reporter can follow. The `duplicate_of` link belongs in `results.md`, which is for Colin.\n\n## Publish or decline\n\nA defect can be real and still not be worth a published GHSA. Answer these before recommending either way:\n\n- **Does the attacker control the value, or only the key?** A write the attacker cannot choose the value of — a fixed object, a truthiness flip — needs a second-order sink in the victim's own code before it does anything. That sink is the application's bug, not the library's.\n- **What actually happens at the boundary?** Test the near-miss cases rather than assuming. A gadget that throws on the interesting inputs and only pollutes on the boring ones is much weaker than the report claims.\n- **Is the supported major reachable without cooperating application code?** If the current version needs an unusual schema shape or an app that derives paths from user input, an advisory naming it overstates the exposure.\n- **Is there a patch to point at?** An advisory against an EOL major with no patch turns every install red in `npm audit`, mostly transitively, with \"upgrade the major\" as the only remedy. That is a large ecosystem cost for little defensive gain.\n- **Is it already public?** A defect with an open issue describing it was never embargoable. Fix it in the open.\n\nState the recommendation plainly and name what would reverse it. Where a reachability judgment rests on how common some application pattern is, say that you reasoned it rather than measured it.\n\nCredit is the thing to offer when declining. A reporter who found a real bug that produced a real fix wants attribution — release-notes credit costs nothing and is usually what they were after.\n\n## The reporter note\n\nPer the `triage` skill's `## Drafting the close comment`, plus one advisory-specific pressure: a decline invites you to walk the reporter through the entire severity argument, and the analysis is right there. Do not paste it. Four things belong in the note — it is fixed and where, whether you are publishing, the single reason that decides it, and the offer of credit. Everything else lives in `results.md`.\n\nCorrect a reporter's overstated impact when it is wrong, in one sentence, with what you actually observed. Do not relitigate their CVSS vector line by line.\n\n<!-- File: .claude/skills/triage/SKILL.md (Tokens: ~5965 | Category: anthropic-skill) -->\n---\nname: triage\ndescription: Investigate a GitHub issue or pull request in colinhacks/zod and write up a durable verdict. Use whenever asked to triage, investigate, review, evaluate, or form an opinion on an issue or PR (by number, URL, or \"the open PR queue\"), and when sweeping many of them in bulk. For a draft SECURITY ADVISORY (a GHSA id, the Security tab, a private vulnerability report) use the `security-advisory` skill instead — it shares this file's conventions but its workflow lands the fix before drafting any comment. Covers where write-ups live on disk (.triage/issues/NNNN/, .triage/prs/NNNN/), the cheap-disqualifier pass that avoids spinning up a worktree for stale PRs, the worktree checkout procedure for PRs, the results.md format that keeps a 250-PR sweep greppable and resumable, how to report back in chat — the final message is all the user sees, so it leads with a code block showing the problem and a short outline of the fix — and the maintainer-voice comment it drafts whenever the verdict is to close.\n---\n\n# Investigating issues and PRs\n\nProduce a write-up a maintainer can act on without re-reading the thread. The durable deliverable is always a file on disk — sweeps span many sessions and agents, so anything living only in a transcript is lost. But the file is not what the user reads first: your final chat message is, and for most tickets it is the only thing they will ever read. Both matter, and `## Reporting back` below is not optional.\n\n**Read-only on GitHub.** Never comment, label, assign, close, review, approve, or merge unless the user explicitly asks in this session. You are forming an opinion, not publishing one. Drafting the close comment described below is part of forming it — writing the text is required, sending it is not yours to do.\n\n## Where things live\n\nEverything goes under `.triage/` in the **root repo**, which is gitignored — write freely, it never shows up in `git status`.\n\n```\n.triage/\n  index.md                    # the original issue sweep index — hand-maintained prose, still the canonical issue queue\n  issues/index.md             # generated from frontmatter by reindex.mjs\n  issues/<N>/results.md       # the write-up; scratch files live beside it\n  prs/index.md                # generated\n  prs/<N>/results.md\n  advisories/<GHSA-ID>/results.md   # draft security advisories from the Security tab\n  signal-report.md            # generated by scripts/triage-signal.ts — ranks the open backlog\n```\n\nOne directory per ticket, named by bare GitHub number (no leading zeros, no `issue-`/`pr-` prefix) — or by bare GHSA id for an advisory. `results.md` is the write-up. Everything else you need — repro scripts, a saved `gh pr diff`, test fixtures, benchmark output, notes — goes in that same directory. Name scratch files descriptively (`repro.ts`, `diff.patch`, `bench-before.txt`); there is no naming rule beyond not colliding with `results.md`.\n\nFor an issue repro, `nub run dev .triage/issues/<N>/repro.ts` from the root repo works directly.\n\nA repro must sit **inside** the checkout it is testing, because module resolution walks up from the file, not from the cwd — a script under `.triage/` cannot resolve `zod` from a PR worktree. Keep the canonical copy in the ticket directory and copy it in to run: `cp .triage/prs/<N>/repro.ts <worktree>/repro-<N>.ts`. That is also what lets you run the identical file against both `main` and the PR and diff the output, which is the cheapest way to characterize a behavior change. For a probe needing GC or other flags, invoke Nub with the flags directly: `nub --expose-gc --conditions=@zod/source repro-<N>.ts`.\n\n### Writing to the root repo from a PR worktree\n\nThis is the one mistake that silently destroys work. A worktree has its **own** gitignored `.triage/`, so a relative write from inside it lands somewhere nobody will ever look.\n\nResolve the root once and always write through it:\n\n```bash\nZOD_ROOT=$(git worktree list | head -1 | awk '{print $1}')   # correct from inside any worktree\nmkdir -p \"$ZOD_ROOT/.triage/prs/<N>\"\n```\n\nEvery path you write must start with `$ZOD_ROOT`. Investigate in the worktree, record in the root.\n\n## results.md format\n\nOpen with YAML frontmatter. It is what makes a 250-PR sweep aggregatable and resumable — an interrupted sweep is resumed by grepping `status`, never by re-reading write-ups.\n\n```yaml\n---\nnumber: 5928\ntype: pr                    # pr | issue\nstatus: done                # in-progress | done\nverdict: merge-with-changes\ntitle: \"fix: preserve brand through .optional()\"\nauthor: someuser\nurl: https://github.com/colinhacks/zod/pull/5928\ninvestigated: 2026-08-08\n---\n```\n\n`verdict` for **PRs** — one of:\n\n- `merge-as-is` — correct, tested, in scope. Say so plainly.\n- `merge-with-changes` — right idea, needs specific edits. Enumerate them concretely enough to apply.\n- `needs-author-changes` — blocked on something only the author can supply (repro, rebase, scope cut).\n- `decline` — out of scope or wrong approach. Give the one or two strongest reasons, plus the escape hatch users actually have.\n- `superseded` — already fixed on `main` or landed via another PR. Name the commit or PR.\n- `needs-maintainer-decision` — the code is fine; the call is a product/API judgment that is Colin's to make. State the tradeoff and your recommendation.\n\n`verdict` for **issues** — reuse the vocabulary already established in `.triage/index.md`: `good-idea`, `already-fixed`, `needs-maintainer-input`, `not-recommended`, `needs-more-reproduction`, `duplicate` (add a `duplicate_of: NNNN` key alongside it).\n\n`verdict` for **advisories** — one of:\n\n- `valid` — a real vulnerability in zod, reproducible on current `main`. State the affected versions and the fix.\n- `not-a-vulnerability` — the behavior is intended, requires the app to already be doing something unsafe, or is a plain bug with no security consequence. This is the most common outcome; say which.\n- `already-fixed` — real, but no longer reproduces on `main`. Name the commit or release that fixed it.\n- `duplicate` — the same defect as another advisory (add `duplicate_of: GHSA-xxxx-xxxx-xxxx`). The `__proto__` reports cluster heavily; check before concluding.\n- `needs-reporter-info` — the report is too vague to reproduce, or the PoC does not run.\n- `needs-maintainer-decision` — real behavior, but whether it counts as a vulnerability is a threat-model call that is Colin's to make.\n\nAdvisory frontmatter swaps `number:` for `ghsa:` and adds `severity_claimed:` (what the reporter asserted) alongside `severity_actual:` (your assessment, or `none`).\n\nSet `status: in-progress` in a stub the moment you start, so a parallel agent or a later session does not duplicate the work. Flip to `done` before you report back.\n\n**One pass. Finish the ticket.** A maintainer question is never a reason to leave a write-up unfinished — investigate fully, commit to a recommended `verdict`, write every section including `## Proposed comment`, and flip `status: done`. Anything genuinely needing the maintainer goes in `## Open questions` (below) *inside the finished file*, so their answer is a one-line amendment to a complete write-up rather than a second investigation. Never hand back a provisional verdict, a `status: in-progress` file, or a promise to finalize later; never wait on an answer before writing. `verdict: TBD` is not a verdict. The only thing that legitimately blocks `done` is evidence you could not obtain — a PR that will not build, a repro that needs credentials — and that is `needs-author-changes` or `needs-more-reproduction`, which are verdicts, not deferrals.\n\nAfter the frontmatter, write prose under these headings. Match the density of the existing issue write-ups in `.triage/issues/*/results.md` — they are the house style; skim one before your first write-up.\n\n- `## Request` / `## Change` — what is actually being asked or changed, in your own words. Include the thread's evolution when maintainers or the author already moved the discussion.\n- `## Current state` — what the code on `main` does **today**, with `file.ts:line` references. Most old tickets were filed against v3 or early v4 and are stale on arrival; this section is usually where the verdict comes from.\n- `## Analysis` — correctness, edge cases, type-level implications, perf, and whether the tests actually cover the claim.\n- `## Recommendation` — the action to take, and the substance of the reply to post. Do not draft it in maintainer voice unless asked, with one standing exception below.\n- `## Proposed comment` — **required whenever the recommendation is to close the ticket**, in a fenced block, ready to paste. See below.\n- `## Open questions` — **only** for calls that are genuinely the maintainer's: product/API direction, a breaking-change tradeoff, taste, or picking between two contributors' PRs. Omit the heading entirely when there are none, which is the common case. Never park a question here that the code, the tests, `git log`, or an existing write-up answers — that is research you owe, not a decision they owe. See below.\n\n### Writing `## Open questions`\n\nEach question is answerable cold, by someone who has not read the write-up and will not open it. One line for what is at stake, then 2–4 lettered options, each with a one-line consequence, with your recommendation marked and placed first. Write in behavior, not identifiers: \"should a schema that declares a field named `__proto__` still reject it?\" not \"should `handleCatchall` precede the `keySet` check?\".\n\nThe verdict above already assumes your recommended option. Say which verdict each *other* option would produce, so the maintainer can see the cost of disagreeing without asking you to redo the analysis:\n\n```markdown\n## Open questions\n\n**Q1 — Should `.strict()` reject an own `__proto__` key, or keep silently dropping it?** Runtime breaking change for anyone posting JSON bodies through `.strict()`.\n\n- **(A) Report it — take the PR.** ← recommended. Restores \"strict rejects every unknown key\". Verdict as written: `merge-with-changes`.\n- (B) Keep dropping it, document the exemption. Zero breakage; the one key an attacker actually sends stays the one key strict does not flag. → `decline`.\n- (C) Report it behind a config flag. No breakage; a permanent knob for a two-line behavior. → `needs-author-changes`.\n```\n\nWhen the answer arrives, amend the file in place: set the final `verdict`, fold the decision into `## Recommendation`, rewrite `## Proposed comment` to match, and leave the question and its answer recorded so nobody relitigates it. `status` stays `done` throughout — it was never not done.\n\n### Drafting the close comment\n\nAny verdict whose action is \"close this\" gets a drafted reply: `decline` and `superseded` on a PR, `not-recommended`, `already-fixed`, and `duplicate` on an issue, and `not-a-vulnerability`, `already-fixed`, and `duplicate` on an advisory. Closing is the one outcome where a human is going to have to say something to a real person who spent time on this, so writing that sentence is part of the triage, not a follow-up task.\n\nFollow the \"Commenting on issues and PRs\" section of AGENTS.md for the maintainer voice. Succinct, conversational, friendly. Lead with the decision, give the one or two strongest reasons, name the escape hatch the user actually has, and close warmly. Two or three sentences is usually the whole thing.\n\nFor an issue reporting that `z.coerce.number()` accepts `null`, the whole draft is:\n\n```markdown\nWorking as intended. `z.coerce.number()` runs `Number()` on the input before it validates, and `Number(null)` is `0` — the coercion happens first, so there is nothing left to reject by the time the number check runs.\n\nIf you want these rejected, use plain `z.number()` and do the coercion at the boundary where you actually know the input is a numeric string. Closing this out — thanks for the writeup 👍\n```\n\nNote what it does not do: no bullet list of every objection, no apology, no \"great question\", no asking whether the reporter agrees. Decision, reason, escape hatch, closer.\n\n#### The investigation's depth never sets the comment's length\n\nThis is the rule that gets broken, and it gets broken hardest on exactly the tickets where the investigation went deepest. A week of benchmarks, a prior-art trail, a prototype, an ecosystem comparison — all of it feels load-bearing, so it leaks into the reply and the two-or-three-sentence bar quietly becomes five paragraphs. **The evidence you are proudest of is the first thing to cut.** It belongs in `results.md`, which is where a maintainer looks when someone pushes back; the comment is not the place to prove you did the work.\n\nA worked case. A draft advisory reported ReDoS in `z.fromJSONSchema()` — real, reproducible, two sinks, exponential. The investigation ran long: measured timings, prior art in a closed issue and a closed fix PR, a 54MB dependency weighed, a zero-dep detector prototyped and evaluated, Ajv's published security contract pulled in as precedent. The drafted comment carried most of that. What the maintainer actually posted, in full:\n\n```markdown\nClosing this out — declining. `z.fromJSONSchema` is designed to convert arbitrary JSON Schema to a Zod schema, and it up to the user to do their own out of band validation. Thanks for the report.\n```\n\nThree sentences, and **this is what most declining comments should look like.** Read what it drops and adopt each as a rule:\n\n- **No cross-references.** No issue number, no PR number, no prior-art trail — even when a maintainer closed the identical report before. That history justifies the decision to *you*; it does not need to be shown to the reporter.\n- **No numbers, no engineering.** No timings, no dependency sizes, no analysis of what a guard would cost or why the cheap version fails. None of it changes the answer.\n- **No external precedent.** How another library documents the same contract is research that settles your verdict, not material for the reply.\n- **The reason is design intent, asserted — not an argument, defended.** \"Is designed to convert arbitrary JSON Schema\" states the contract in one clause and stops. A maintainer declares scope; they do not litigate it. Once you find yourself building a case, you have written the wrong document.\n- **No concession and no promise.** Do not offer \"you have a fair point about X\", and do not commit to a docs change, a new option, or any follow-up. Real follow-ups get filed as their own issue; a decline that promises work is no longer a decline.\n\nKeep only: the decision, one clause of reason, what the user does instead (\"their own out of band validation\"), and a short closer. When a drafted comment runs past three or four sentences, that is the signal to cut, not to reorganize.\n\nDrafting is not posting. The draft sits in `results.md` and in your chat message; you never send it unless the user explicitly asks in this session. For an advisory the draft is the note to the reporter, and it must not restate an unpatched exploit.\n\nThe length rule binds hardest on an advisory decline, which is where it is most often broken: a reporter who filed a real, reproducible defect invites you to walk them through the whole severity argument, and the analysis you just spent hours on is right there. Do not paste it. The reporter needs four things — it is fixed and where, you are not publishing, the one reason that actually decides it, and the offer of credit. Everything else belongs in `results.md`. A decline that runs to six paragraphs is a worse decline, not a more rigorous one.\n\n## Reporting back\n\n`results.md` is the archive. **Your final chat message is the interface, and for most tickets it is the only thing the user will ever read.** Assume they see it hours later, in a queue, without the thread in front of them, without opening the file, and without any of your working context. \"Investigated #5928, write-up is in `.triage/prs/5928/results.md`\" has delivered nothing — it just moved the work onto them.\n\nSo the last message before you rest always carries these three things, in this order, plus a fourth when the verdict is to close:\n\n**1. The problem, as a code block.** Not a paragraph describing the code block — the actual minimal snippet, with the wrong result written next to it as a comment. This is the single highest-value thing in the message: it is what lets the user confirm or reject your read in five seconds instead of re-deriving the whole ticket. Strip the reporter's app scaffolding down to the schema and the one call that misbehaves, and show actual versus expected.\n\n```ts\nconst schema = z.coerce.number();\n\nschema.parse(null); // → 0\nschema.parse(\"\"); // → 0\nschema.parse([]); // → 0\n// reporter expected each of these to fail validation\n```\n\nType-level tickets get the same treatment, with the inferred type as a comment instead of the parse result:\n\n```ts\ntype Out = z.infer<typeof schema>;\n// actual:   string | undefined\n// expected: string\n```\n\n**2. The shape of the fix, in two or three sentences.** Where it goes (`file.ts:line`), what changes, and how big it really is — one-line type tweak, new method on `ZodType`, or a refactor that touches every wrapper. If the fix is not worth doing, say what the user does instead. Do not paste the patch; the user is deciding whether to spend time here, not applying it from chat.\n\n**3. The verdict and the path.** The frontmatter `verdict` verbatim, plus a link to the ticket and the `results.md` path for the detail you left out.\n\n**4. The proposed comment, if the verdict is to close.** Repeat the `## Proposed comment` draft in the message, in its own fenced block, so the user can read and paste it without opening the file. Say plainly that you have not posted it.\n\n**5. The open questions, if the write-up has any.** Repeat them from `## Open questions`, options and all, so they are answerable without opening the file. Frame them as what they are — the ticket is triaged and the verdict is recorded; the answer only confirms or redirects it. Never present them as a request for permission to finish, never say the write-up is waiting on them, and never end on \"send me your answer and I'll finalize\". The work is already done.\n\nKeep the whole thing short — the code block plus a few sentences. The message is a summary that stands alone, not a condensed replay of the write-up, and it is not the place for the investigation narrative, the commands you ran, or the branches you ruled out. Those are what the file is for.\n\nTwo adjustments by shape of the task:\n\n- **Nothing reproduced** — an `already-fixed` or `not-a-vulnerability` verdict still gets a code block. Show the reporter's own snippet and the *correct* output it now produces on `main`; that is the evidence, and without it the user just has your word.\n- **A bulk sweep** — one summary table of ticket, verdict, and a one-line reason, and code blocks only for the two or three tickets that genuinely need a decision. Twenty code blocks is the same as none. Every close still gets its drafted comment written into its own `results.md`; the message just says which tickets have one waiting instead of inlining them all.\n- **More than one ticket has a comment to post** — pair each one: the link on its own line, its fenced comment block **directly underneath it**, then the next link. Never collect the links in one place and the comment blocks in another, and never make the user match a block to a link by reading the prose between them. This is how a set of draft advisories is always handed back, since the user posts them one at a time with the link and its text side by side.\n\n  ```markdown\n  ### [GHSA-xxxx-xxxx-xxxx](https://github.com/colinhacks/zod/security/advisories/GHSA-xxxx-xxxx-xxxx) — valid\n\n  <the comment for that advisory, in its own fenced block>\n\n  ### [GHSA-yyyy-yyyy-yyyy](https://github.com/colinhacks/zod/security/advisories/GHSA-yyyy-yyyy-yyyy) — duplicate\n\n  <the comment for that advisory, in its own fenced block>\n  ```\n\n## Investigating an issue\n\nNo worktree — read the code on `main`.\n\n1. `gh issue view <N> -R colinhacks/zod --comments` and read the whole thread, including maintainer replies. A prior decision from Colin usually settles it.\n2. Check for duplicates and prior art: `gh search issues --repo colinhacks/zod '<keywords>'`, and grep `.triage/issues/*/results.md` — the sweep may already cover it. Cross-link with a relative `../NNNN/results.md`.\n3. Reproduce against current source before believing the report. Write the repro to `.triage/issues/<N>/repro.ts` and run `nub run dev` on it. A large share of open issues are already fixed.\n4. Brainstorm before concluding. Consider the fix, its blast radius, whether it belongs in core at all, and what userland escape hatch already exists.\n5. Write `results.md`, then report back per `## Reporting back`.\n\n## Investigating a draft advisory\n\n**Use the `security-advisory` skill instead — invoke it before you start.** Draft advisories live in the Security tab, so nothing in `gh issue list` surfaces them, and the workflow diverges from this one in ways that matter: the report is private in both directions, the burden of proof sits with the reporter, the fix must be **landed before any comment is drafted**, and \"real defect\" and \"publishable vulnerability\" are separate calls.\n\nWhat carries over from this skill: where files live, the `results.md` frontmatter and headings, the close-comment voice, and `## Reporting back`. Advisory write-ups go to `.triage/advisories/<GHSA-ID>/results.md` with `ghsa:` in place of `number:`.\n\nNever accept, publish, decline, or comment on an advisory. The write-up and the drafted note are the deliverables; the disposition is Colin's.\n\n## Investigating a PR\n\nCheap pass first — most of a 250-PR backlog is resolved without ever checking out code.\n\n```bash\ngh pr view <N> -R colinhacks/zod --json number,title,author,state,isDraft,mergeable,mergeStateStatus,baseRefName,headRefName,headRepositoryOwner,maintainerCanModify,createdAt,updatedAt,additions,deletions,changedFiles,labels,comments\ngh pr diff <N> -R colinhacks/zod\n```\n\nDisqualify early and write the write-up straight from this, no worktree, when the diff is small and self-evident, or when the PR targets v3 / duplicates a landed change / is already superseded by `main` / is abandoned with unresolved conflicts. Verify `superseded` against actual code on `main` — do not infer it from dates.\n\nOtherwise check it out (this repo's convention, per AGENTS.md):\n\n```bash\ngit fetch origin pull/<N>/head:pr-<N>\ngit worktree add ~/.cursor/worktrees/zod/pr-<N> pr-<N>\ncd ~/.cursor/worktrees/zod/pr-<N>\nnub install --frozen-lockfile      # fast, Nub's store is shared\n```\n\nDo **not** use `gh pr checkout --detach` — it detaches your current working tree instead of creating a worktree.\n\nThen, in the worktree:\n\n1. Read the diff in full context, not just the hunks. What did it touch that the author did not think about?\n2. `git log --oneline main..HEAD` — is this rebased on anything current, or written against a version of the code that has since moved?\n3. Run the tests the change implicates: `nub exec --node vitest run <path>`. Run the full suite only when the change is broad.\n4. Judge test coverage against the repo's own bar: a feature or bug fix without a test is incomplete, and tests must be TypeScript.\n5. Probe the edge cases the author's tests skip — write them into `$ZOD_ROOT/.triage/prs/<N>/` and run them. A passing suite over a shallow test is not evidence.\n6. Write `$ZOD_ROOT/.triage/prs/<N>/results.md`, then report back per `## Reporting back`.\n\n**Clean up when done**, or worktrees accumulate across a large sweep:\n\n```bash\ncd \"$ZOD_ROOT\"\ngit worktree remove ~/.cursor/worktrees/zod/pr-<N> && git branch -D pr-<N>\n```\n\nKeep the worktree only when the verdict is `merge-with-changes` and you have been asked to push the changes; AGENTS.md covers pushing back to a contributor's fork, including preserving their commits.\n\n## Sweeping in bulk\n\nSeed the queue, then work it:\n\n```bash\ngh pr list -R colinhacks/zod --state open --limit 400 --json number,title,updatedAt\n```\n\nResume by asking disk, not memory:\n\n```bash\ngrep -l 'status: done' .triage/prs/*/results.md | wc -l                    # progress\ngrep -h '^verdict:' .triage/prs/*/results.md | sort | uniq -c              # verdict spread\nnub --node .claude/skills/triage/scripts/reindex.mjs prs             # regenerate .triage/prs/index.md\n```\n\n`reindex.mjs` rebuilds an index table from frontmatter, so the index is derived and never hand-edited for PRs. Pass `issues`, `prs`, or `advisories`.\n\n`nub run dev scripts/triage-signal.ts` regenerates `.triage/signal-report.md`, which ranks the whole open backlog by engagement, flags security content, and groups likely-duplicate clusters. Start a sweep there rather than at the top of `gh pr list` — roughly a third of the open backlog is duplicates, and the clusters collapse many tickets into one decision.\n\nWhen parallelizing across sub-agents, give each agent a disjoint set of numbers and have each write only its own ticket directories. Never have two agents share a results file.\n\n<!-- File: packages/docs/.vscode/settings.json (Tokens: ~120 | Category: anthropic-skill) -->\n{\n  \"editor.defaultFormatter\": \"biomejs.biome\",\n  \"editor.formatOnSave\": true,\n  \"editor.codeActionsOnSave\": {\n    \"source.organizeImports.biome\": \"explicit\",\n    \"source.fixAll\": \"always\",\n    \"quickfix.biome\": \"explicit\"\n  },\n  \"editor.wordWrap\": \"wordWrapColumn\",\n  \"editor.wordWrapColumn\": 120,\n  \"[mdx]\": {\n    \"editor.defaultFormatter\": \"unifiedjs.vscode-mdx\",\n    \"editor.formatOnSave\": true\n  }\n}\n\n<!-- File: packages/docs/content/meta.json (Tokens: ~97 | Category: anthropic-skill) -->\n{\n  \"root\": true,\n  \"pages\": [\n    \"---Documentation---\",\n    \"index\",\n    \"basics\",\n    \"api\",\n    \"error-customization\",\n    \"error-formatting\",\n    \"metadata\",\n    \"json-schema\",\n    \"codecs\",\n    \"compile\",\n    \"ecosystem\",\n    \"library-authors\",\n    \"---Packages---\",\n    \"packages/zod\",\n    \"packages/mini\",\n    \"packages/core\"\n  ]\n}\n\n<!-- File: packages/docs/package.json (Tokens: ~455 | Category: anthropic-skill) -->\n{\n  \"name\": \"@zod/docs\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"nub exec --node next build --turbopack\",\n    \"dev\": \"nub exec --node next dev\",\n    \"start\": \"nub exec --node next start\",\n    \"postinstall\": \"nub exec --node fumadocs-mdx\"\n  },\n  \"dependencies\": {\n    \"@inkeep/cxkit-react\": \"^0.5.117\",\n    \"@radix-ui/react-accordion\": \"^1.2.4\",\n    \"@vercel/analytics\": \"^1.5.0\",\n    \"@vercel/og\": \"^0.6.8\",\n    \"fumadocs-core\": \"15.1.0\",\n    \"fumadocs-mdx\": \"11.5.6\",\n    \"fumadocs-ui\": \"15.1.0\",\n    \"gray-matter\": \"^4.0.3\",\n    \"lucide-react\": \"^0.483.0\",\n    \"next\": \"15.5.25\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-tweet\": \"^3.3.1\",\n    \"remark\": \"^15.0.1\",\n    \"remark-gfm\": \"^4.0.1\",\n    \"remark-mdx\": \"^3.1.0\",\n    \"remark-stringify\": \"^11.0.0\",\n    \"shiki\": \"^3.8.1\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"^4.2.4\",\n    \"@types/mdx\": \"^2.0.13\",\n    \"@types/node\": \"22.13.10\",\n    \"@types/react\": \"^19.0.10\",\n    \"@types/react-dom\": \"^19.0.4\",\n    \"postcss\": \"^8.5.12\",\n    \"tailwindcss\": \"^4.2.4\",\n    \"typescript\": \"^5.8.2\"\n  }\n}\n\n<!-- File: packages/docs/tsconfig.json (Tokens: ~243 | Category: anthropic-skill) -->\n{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"paths\": {\n      \"@/.source\": [\"./.source/index.ts\"],\n      \"@/*\": [\"./*\"]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"types\": [\"mdx\", \"node\", \"react\", \"react-dom\", \"@vercel/og\"]\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\", \"components/if.tsx\", \"types/**/*.d.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n\n<!-- File: packages/docs/tsconfig.test.json (Tokens: ~40 | Category: anthropic-skill) -->\n{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"types\": [\"node\", \"vitest\"]\n  },\n  \"include\": [\"**/*.test.ts\"]\n}\n\n<!-- File: packages/docs/vercel.json (Tokens: ~134 | Category: anthropic-skill) -->\n{\n  \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n  \"installCommand\": \"bash -o pipefail -c 'curl -fsSL https://raw.githubusercontent.com/nubjs/nub/0ae8783f1f93763c56dfc827892cfb679e8a0a77/install.sh | bash -s -- 0.8.3 && export PATH=\\\"$HOME/.nub/bin:$PATH\\\" && cd ../.. && nub install --frozen-lockfile'\",\n  \"buildCommand\": \"export PATH=\\\"$HOME/.nub/bin:$PATH\\\" && nub run build\"\n}\n\n<!-- File: packages/bench/typia-case/README.md (Tokens: ~273 | Category: plugin-manifest) -->\n# typia case\n\ntypia generates its validators with a TypeScript transformer, so this directory holds a prebuilt artifact rather than a source import. `build/index.cjs` is generated from `src/index.ts`.\n\nThe transformer does not run inside this workspace: `tspc` live mode crashes on typia's file transformer, and the alternative — `ts-patch install` — patches the TypeScript that every package here shares. The artifact is therefore built in a throwaway project and copied in. The generated file inlines every check, so it depends only on the `typia` runtime already in this package.\n\nTo regenerate:\n\n```sh\nmkdir -p /tmp/typia-build/src && cd /tmp/typia-build\nnub init -y --js --no-install --no-git\nnub add typia@9.7.2 typescript@5.9.3 ts-patch@3.3.0\ncp <repo>/packages/bench/typia-case/src/index.ts src/index.ts\ncp <repo>/packages/bench/typia-case/tsconfig.json tsconfig.json\nnub exec --node ts-patch install && nub exec --node tsc -p tsconfig.json\ncp build/index.js <repo>/packages/bench/typia-case/build/index.cjs\n```\n\n<!-- File: packages/docs/README.md (Tokens: ~49 | Category: plugin-manifest) -->\n# onepager-fuma\n\n<Tabs groupId=\"lib\" items={[\"Zod\", \"Zod Mini\"]}>\n<Tab value=\"Zod\">\n</Tab>\n<Tab value=\"Zod Mini\">\n</Tab>\n</Tabs>\n\n<!-- File: packages/mini/README.md (Tokens: ~160 | Category: plugin-manifest) -->\n# @zod/mini\n\nZod Mini as a standalone package. It re-exports [`zod/mini`](https://zod.dev/packages/mini) from a peer dependency on `zod`, so every schema it creates is a `zod` schema.\n\n```sh\nnpm install zod @zod/mini\n```\n\n```ts\nimport * as z from \"@zod/mini\";\n\nconst User = z.object({\n  name: z.string(),\n  age: z.number().check(z.int(), z.positive()),\n});\n\nz.parse(User, { name: \"Colin\", age: 30 });\n```\n\nThe version tracks `zod`: each `@zod/mini@4.x.y` requires `zod@^4.x.0`.\n\n<!-- File: packages/resolution/README.md (Tokens: ~62 | Category: plugin-manifest) -->\n# Package resolution checks\n\nRun from the repository root:\n\n```sh\nnub run build\nnub run --filter @zod/resolution test:all\n```\n\nThese checks inspect the published declarations with Are the Types Wrong and execute the built CommonJS and ES modules on plain Node through Nub.\n\n<!-- File: packages/tsc/README.md (Tokens: ~62 | Category: plugin-manifest) -->\n# Zod perftesting\n\nRun `nub run generate` to generate random Zod schemas.\n\nRun `nub run build:bench` to run `tsc` with `extendedDiagnostics`.\n\nThe workspace dependency points to the local Zod package. Rebuild Zod after changing its typings.\n\n<!-- File: packages/zod/README.md (Tokens: ~1932 | Category: plugin-manifest) -->\n<p align=\"center\">\n  <img src=\"logo.svg\" width=\"200px\" align=\"center\" alt=\"Zod logo\" />\n  <h1 align=\"center\">Zod</h1>\n  <p align=\"center\">\n    TypeScript-first schema validation with static type inference\n    <br/>\n    by <a href=\"https://x.com/colinhacks\">@colinhacks</a>\n  </p>\n</p>\n<br/>\n\n<p align=\"center\">\n<a href=\"https://github.com/colinhacks/zod/actions?query=branch%3Amain\"><img src=\"https://github.com/colinhacks/zod/actions/workflows/test.yml/badge.svg?event=push&branch=main\" alt=\"Zod CI status\" /></a>\n<a href=\"https://opensource.org/licenses/MIT\" rel=\"nofollow\"><img src=\"https://img.shields.io/github/license/colinhacks/zod\" alt=\"License\"></a>\n<a href=\"https://www.npmjs.com/package/zod\" rel=\"nofollow\"><img src=\"https://img.shields.io/npm/dw/zod.svg\" alt=\"npm\"></a>\n<a href=\"https://discord.gg/KaSRdyX2vc\" rel=\"nofollow\"><img src=\"https://img.shields.io/discord/893487829802418277?label=Discord&logo=discord&logoColor=white\" alt=\"discord server\"></a>\n<a href=\"https://github.com/colinhacks/zod\" rel=\"nofollow\"><img src=\"https://img.shields.io/github/stars/colinhacks/zod\" alt=\"stars\"></a>\n</p>\n\n<div align=\"center\">\n  <a href=\"https://zod.dev/api\">Docs</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://discord.gg/RcG33DQJdf\">Discord</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://twitter.com/colinhacks\">𝕏</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://bsky.app/profile/zod.dev\">Bluesky</a>\n  <br />\n</div>\n\n<br/>\n<br/>\n\n### [Read the docs →](https://zod.dev/api)\n\n<br/>\n<br/>\n\n## What is Zod?\n\nZod is a TypeScript-first validation library. Define a schema and parse some data with it. You'll get back a strongly typed, validated result.\n\n```ts\nimport * as z from \"zod\";\n\nconst User = z.object({\n  name: z.string(),\n});\n\n// some untrusted data...\nconst input = {\n  /* stuff */\n};\n\n// the parsed result is validated and type safe!\nconst data = User.parse(input);\n\n// so you can use it with confidence :)\nconsole.log(data.name);\n```\n\n<br/>\n\n## Features\n\n- Zero external dependencies\n- Works in Node.js and all modern browsers\n- Tiny: `2kb` core bundle (gzipped)\n- Immutable API: methods return a new instance\n- Concise interface\n- Works with TypeScript and plain JS\n- Built-in JSON Schema conversion\n- Extensive ecosystem\n\n<br/>\n\n## Installation\n\n```sh\nnpm install zod\n```\n\n<br/>\n\n## Basic usage\n\nBefore you can do anything else, you need to define a schema. For the purposes of this guide, we'll use a simple object schema.\n\n```ts\nimport * as z from \"zod\";\n\nconst Player = z.object({\n  username: z.string(),\n  xp: z.number(),\n});\n```\n\n### Parsing data\n\nGiven any Zod schema, use `.parse` to validate an input. If it's valid, Zod returns a strongly-typed _deep clone_ of the input.\n\n```ts\nPlayer.parse({ username: \"billie\", xp: 100 });\n// => returns { username: \"billie\", xp: 100 }\n```\n\n**Note** — If your schema uses certain asynchronous APIs like `async` [refinements](https://zod.dev/api#refinements) or [transforms](https://zod.dev/api#transforms), you'll need to use the `.parseAsync()` method instead.\n\n```ts\nconst schema = z.string().refine(async (val) => val.length <= 8);\n\nawait schema.parseAsync(\"hello\");\n// => \"hello\"\n```\n\n### AOT compilation\n\nFor hot validation paths, `z.compile(schema)` returns a schema clone with an ahead-of-time compiled fast path. Valid inputs take the compiled path; invalid inputs fall back to the regular parser so error reporting stays identical.\n\nAcross a 55-schema benchmark the median speedup is **2.4x**, and it scales with how much work the schema does per parse: a large array of objects is ~9x, a 20-key object ~9x, a nested object ~4.5x, while a bare `z.string()` gains nothing — compilation removes per-node dispatch and allocation, and a single `typeof` has none to remove.\n\n```ts\nconst CompiledPlayer = z.compile(Player);\n\nCompiledPlayer.parse({ username: \"billie\", xp: 100 });\n```\n\nTo enable compilation globally for schemas constructed after import:\n\n```ts\nimport \"zod/compile\"; // place before modules that define schemas\n```\n\nThings to know:\n\n- Compilation uses `new Function`. Global mode is automatically disabled when `z.config({ jitless: true })` is set (e.g. CSP environments); calling `z.compile()` directly is an explicit opt-in.\n- Schemas with async refinements or transforms can't be compiled, and neither can a few other constructs. That is not an error: `z.compile()` hands the schema back unchanged and it keeps using the regular parser, exactly as global mode leaves it. Pass `{ strict: true }` to throw `ZodCompileAsyncError` / `ZodCompileUnsupportedError` instead.\n- On invalid input, refinements and transforms may run twice (fast path, then fallback).\n- Deriving a new schema from a compiled one (`.refine()`, `.extend()`, etc.) returns an uncompiled schema — compile the final schema.\n\nSee [`compile` docs](https://zod.dev/compile) for details.\n\n### Handling errors\n\nWhen validation fails, the `.parse()` method will throw a `ZodError` instance with granular information about the validation issues.\n\n```ts\ntry {\n  Player.parse({ username: 42, xp: \"100\" });\n} catch (err) {\n  if (err instanceof z.ZodError) {\n    err.issues;\n    /* [\n      {\n        expected: 'string',\n        code: 'invalid_type',\n        path: [ 'username' ],\n        message: 'Invalid input: expected string, received number'\n      },\n      {\n        expected: 'number',\n        code: 'invalid_type',\n        path: [ 'xp' ],\n        message: 'Invalid input: expected number, received string'\n      }\n    ] */\n  }\n}\n```\n\nTo avoid a `try/catch` block, you can use the `.safeParse()` method to get back a plain result object containing either the successfully parsed data or a `ZodError`. The result type is a [discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions), so you can handle both cases conveniently.\n\n```ts\nconst result = Player.safeParse({ username: 42, xp: \"100\" });\nif (!result.success) {\n  result.error; // ZodError instance\n} else {\n  result.data; // { username: string; xp: number }\n}\n```\n\n**Note** — If your schema uses certain asynchronous APIs like `async` [refinements](https://zod.dev/api#refinements) or [transforms](https://zod.dev/api#transforms), you'll need to use the `.safeParseAsync()` method instead.\n\n```ts\nconst schema = z.string().refine(async (val) => val.length <= 8);\n\nawait schema.safeParseAsync(\"hello\");\n// => { success: true; data: \"hello\" }\n```\n\n### Inferring types\n\nZod infers a static type from your schema definitions. You can extract this type with the `z.infer<>` utility and use it however you like.\n\n```ts\nconst Player = z.object({\n  username: z.string(),\n  xp: z.number(),\n});\n\n// extract the inferred type\ntype Player = z.infer<typeof Player>;\n\n// use it in your code\nconst player: Player = { username: \"billie\", xp: 100 };\n```\n\nIn some cases, the input & output types of a schema can diverge. For instance, the `.transform()` API can convert the input from one type to another. In these cases, you can extract the input and output types independently:\n\n```ts\nconst mySchema = z.string().transform((val) => val.length);\n\ntype MySchemaIn = z.input<typeof mySchema>;\n// => string\n\ntype MySchemaOut = z.output<typeof mySchema>; // equivalent to z.infer<typeof mySchema>\n// number\n```\n\n<!-- File: .cursorrules (typescript-zod-tailwind-nextjs-cursorrules-prompt-.mdc) (Tokens: ~488 | Category: cursor-rule) -->\n---\ndescription: \"Cursor rules for TypeScript development with Zod, Tailwind, and Next.js integration.\"\nglobs: **/*\nalwaysApply: false\n---\n# Coding Style Guide\n\nCode Style and Structure:\n- Write concise, technical TypeScript code with accurate examples\n- Use functional and declarative programming patterns; avoid classes\n- Prefer iteration and modularization over code duplication\n- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError)\n- Structure files: exported component, subcomponents, helpers, static content, types\n\nNaming Conventions:\n- Use lowercase with dashes for directories (e.g., components/auth-wizard)\n- Favor named exports for components\n\nTypeScript Usage:\n- Use TypeScript for all code; prefer interfaces over types\n- Avoid enums; use maps instead\n- Use functional components with TypeScript interfaces\n- Use Zod for form validation\n\nSyntax and Formatting:\n- Use the \"function\" keyword for pure functions\n- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements\n- Use declarative JSX\n\nError Handling and Validation:\n- Prioritize error handling: handle errors and edge cases early\n- Use early returns and guard clauses\n- Implement proper error logging and user-friendly messages\n- Use Zod for form validation\n- Model expected errors as return values in Server Actions\n- Use error boundaries for unexpected errors\n\nUI and Styling:\n- Use Shadcn UI, Radix, and Tailwind Aria for components and styling\n- Implement responsive design with Tailwind CSS; use a desktop-first approach\n\nPerformance Optimization:\n- Minimize 'useEffect', and 'setState'; favor React Remix Components (RSC)\n- Wrap client components in Suspense with fallback\n- Use dynamic loading for non-critical components\n- Optimize images: use WebP format, include size data, implement lazy loading\n\nKey Conventions:\n- Use proper URL search parameter state management\n- Optimize Web Vitals (LCP, CLS, FID)\n- Limit 'use client'\n\nWhen React Server Components (RSC) are used:\n- Favor server components and Next.js SSR\n- Use only for Web API access in small components\n- Avoid for data fetching or state management\n\nFollow React Remix docs for Data Fetching, Rendering, and Routing\n\nFollow Next.js docs for Data Fetching, Rendering, and Routing when Next JS is used instead of React Remix\n</yakaai_skills>"}