Edit-time docs style enforcement via Vale + PostToolUse hook#36
Conversation
Inconsistent voice and AI slop survived our PR-time-only Vale check. This adds: - PostToolUse hook in .claude/settings.json that runs Vale on .mdx edits, surfacing violations immediately instead of at PR review time - New FirstPersonPlural Vale rule blocking "we"/"our"/"let's" — the hardest manual rule from prior writing feedback - Remove AllowsYouTo rule; proselint + write-good cover the patterns - Slim CLAUDE.md to project context + hard constraints + skill pointer The full prose style guide moves to a workspace-level writing-docs skill (separate change). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The PostToolUse hook would error on every .mdx edit because Vale needs a separate mdx2vast binary to parse MDX, which isn't bundled with the brew install. Treating .mdx as Markdown via [formats] lets Vale lint prose with zero external dependencies. CI is unaffected — errata-ai/vale-action installs its own MDX parser. Verified locally: FirstPersonPlural, Slop, and Microsoft.We all fire on a test .mdx fixture as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Same diagnosis as the Vale hook commit: voice and structure feedback weren't landing consistently because nothing forced Claude to load the existing prose guide before writing. This wraps the guide as a Claude Code Skill with progressive disclosure (short SKILL.md + on-demand references): - SKILL.md: 10-step workflow (clarify, load refs, match brief, read source, read neighbors, outline, draft, quality gates, vale, preview). Auto-triggers on docs/**/*.mdx work. - references/style-guide.md: 7 principles, anti-slop list, sentence rules, style exemplar - references/page-briefs.md: per-page creative briefs for Getting Started, Guides, API Reference, Configuration - references/quality-gates.md: 7-gate pre-merge checklist + Vale enforcement notes - references/source-files.md: critical source files map + role availability table + protocol facts The skill points at the PostToolUse hook added earlier on this branch so Vale runs automatically after each .mdx edit and Claude self-corrects before the user sees the draft. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The writing-docs skill now lives at the monorepo workspace .claude/ skills/ so it auto-loads from any cwd. Keeping a second copy here just creates a sync burden. CLAUDE.md updated to point at "the writing-docs skill" by name rather than by path. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Subdirectory .claude/settings.json files aren't documented to load in Claude Code (only workspace root and ~/.claude/settings.json). The Vale hook now lives at workspace .claude/settings.json with an 'if' filter scoping it to docs/**/*.mdx — same effect, actually supported. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Custom Vale rule duplicated the Microsoft style pack's `Microsoft.We` rule, which catches the same we/our/let's surface. Keeping both produces double warnings on every match. Stock pack wins. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Vale's default spell-check flags every project-specific term as unknown (config, Sepolia, Hono, middleware, USDC, x402r, etc). Adding a Vocab file at styles/config/vocabularies/x402r/accept.txt silences the false positives without disabling the rule entirely. Also gitignore the Microsoft/proselint/write-good style packs — those are fetched via `vale sync` per machine and shouldn't live in the repo. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Tried this out locally on this branch. Setup was clean: One real finding: the vocab in On
On
These aren't slop, they're load-bearing nouns, so they shouldn't fire. Without adding them, the PostToolUse hook will dump a wall of false-positive spelling errors into Claude's context on every Suggest extending |
Default error severity drowns out style/voice rules whenever a page contains chain names, identifiers, or contract names (Ethereum, Arbitrum, pluggable, combinators, ABIs, etc). Per A1igator's PR review, the noise floods the PostToolUse hook output and buries actually-useful rules (Microsoft.Dashes, Microsoft.We, etc). Follows GitLab's tiered Vale convention: error = render-breaking, warning = style guide, suggestion = preference. Spelling false positives don't break builds and don't make sense as the loudest signal. Real-typo detection lives in editor review. The existing vocab file stays — it now serves Vale.Terms (case enforcement, e.g. flags 'hono' → 'Hono', 'sdk' → 'SDKs?') which is genuinely useful and not what the downgrade addresses. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Good catch. Verified the same flood on Took a different path than vocab expansion in Reasoning:
Side benefit: the existing vocab file stops being a maintenance liability but stays useful — Vale.Terms now uses it for case enforcement (e.g. flags After-state on those pages: 14 real errors (em dashes, acronyms in headings) + 7 warnings (wordiness, etc.) + 0 spelling-noise. Clean signal. Let me know if you'd rather we add the vocab anyway as belt-and-suspenders. |
|
Solid reasoning on root-cause vs vocab-treadmill, and the One concern with the all-or-nothing downgrade: Three cheaper alternatives worth considering:
Lean toward (1) personally, it's the smallest change and preserves the "spelling is real but lower priority" signal without the floor. But any of the three beats "no spell-check anywhere." |
Two-part fix for the noise/detection tradeoff per A1igator's PR review: 1. Vale.Spelling: suggestion -> warning. The earlier suggestion-tier downgrade combined with MinAlertLevel = warning silently dropped spelling from both hook output and CI, regressing typo detection to "editor review only". At warning level spelling still surfaces (catches recieve/transferd/unkown) but ranks below errors so it doesn't dominate. 2. Seed chain-name vocab from x402rChains in x402r-sdk/packages/core/src/config/index.ts. Bounded source of truth: only chains with deployed contracts get accepted (currently just Base; Sepolia was already in vocab). Regen one-liner lives in accept.txt so the next chain deploy triggers a clear vocab update. Per-the-investigation finding: the noise A1igator was seeing on docs/index.mdx (Ethereum, Arbitrum, Celo, Linea) isn't masked by this vocab seed because those chains aren't in x402rChains -- the docs/index.mdx Supported Networks table lists 11 networks while code deploys to 2. Those warnings are now correctly tagged as warnings, not errors, and serve as signal that the table is aspirational. Separate concern from this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
You're right on both counts and I had it backwards. Pushed 1. 2. Seeded chain vocab from Interesting finding while doing this: That's actually the right outcome: those warnings are now correctly signal, not noise. The table is aspirational. Worth a separate doc-accuracy PR. After-state on |
|
Ran Vale tree-wide on Totals250 errors / 543 warnings / 1 suggestion across 66 MDX files. Severity x rule:
Real issue introduced by this PR:
|
| Vocab entry | Demand | Reality |
|---|---|---|
SDKs? |
"Use 'SDKs?' instead of 'sdk'" |
24x. The regex ? leaks through as literal replacement text. |
paymentInfo |
"Use 'paymentInfo' instead of 'PaymentInfo'" |
10x. Flags the legitimate TS type name. |
Base (chain) |
"Use 'Base' instead of 'base'" |
8x. Fires on the base code identifier (chain key, imports). |
PaymentInfoHash |
"Use 'PaymentInfoHash' instead of 'paymentInfoHash'" |
2x. Type vs variable collision. |
[Bb]igint |
"Use '[Bb]igint' instead of 'BigInt'" |
1x. Would suggest the JS built-in be replaced with a regex string. |
Hono |
"Use 'Hono' instead of 'hono'" |
1x. Flags the npm package name in import paths. |
async |
"Use 'async' instead of 'Async'" |
1x. |
Examples:
contracts/periphery/auth-capture-escrow.mdx:68:131:Vale.Terms:Use 'paymentInfo' instead of 'PaymentInfo'.
contracts/examples.mdx:8:313:Vale.Terms:Use 'SDKs?' instead of 'sdk'.
roadmap.mdx:62:68:Vale.Terms:Use 'Hono' instead of 'hono'.
contracts/periphery/refund-request.mdx:86:35:Vale.Terms:Use 'PaymentInfoHash' instead of 'paymentInfoHash'.
Fix options (any combination):
- Restrict
Vale.TermstosuggestionorNOin.vale.iniso vocab is spelling-only. - Strip regex chars from
accept.txt: splitSDKs?intoSDK+SDKs, replace[Bb]igintwithbigint+Bigint, etc. - Drop entries whose case is genuinely ambiguous between proper noun and code identifier:
Base,paymentInfo,PaymentInfoHash,async,Hono. Let them ride as ignored spelling instead of enforced canonical case.
Em dashes (137 errors)
CLAUDE.md hard-bans em dashes but they're spread across 30+ files. Worst offenders:
20 x402-integration/escrow-scheme.mdx
14 sdk/client/quickstart.mdx
14 contracts/gas-costs.mdx
10 sdk/arbiter/quickstart.mdx
8 roadmap.mdx
8 contracts/conditions/overview.mdx
7 contracts/license.mdx
7 contracts/fees.mdx
7 contracts/audits.mdx
6 contracts/recorders/overview.mdx
5 contracts/conditions/escrow-period.mdx
4 index.mdx
3 sdk/arbiter/ai-integration.mdx
3 contracts/conditions/freeze.mdx
3 contracts/conditions/custom.mdx
This PR exposes them but doesn't fix them. Separate cleanup PR territory, but worth calling out as the scope outside index.mdx + roadmap.mdx.
Spelling warnings (263, 108 unique words)
Many are stable technical terms worth batch-adding to vocab:
- Already-canonical primitives:
reentrancy,multisig,timelock(s),timelocked,permissionless,mempool,combinator(s),pluggable,composable,composability,dApps,gwei,calldata,hardcoded,untrusted,trustlessness,subgraph,testnet(s),escrowed,Gasless,Micropayments,Swappable,deduplication,deserialize,Deregister - Proper nouns:
Coinbase,Spearbit,Solady,Ownable,Uniswap,Aave,Mintlify,Anthropic,Windsurf,Arweave,Flashbots,Licensor,Abdoli,Vrajang,Parikh - Acronym plurals:
LLMs,APIs,ABIs,CIDs,UIs,URIs,txns - Tooling:
npm,npx,pnpm,mkdir,tsx,repo,dotenv,multicall - Type/identifier names appearing in prose (could be backticked instead, but currently bare):
ICondition,IRecorder,arbiterAddress,arbiterCondition,authorizeCondition,authorizeRecorder,chargeCondition,chargeRecorder,releaseCondition,releaseRecorder,freezeAddress,freezeBy,freezeDuration,freezePayment,freezePolicy,isFrozen,unfreezePayment,escrowAddress,escrowPeriod,releaseBy,paymentExists,paymentId,requestRefund,collectorData,feeCalculator,feeRecipient,txHashes,existingCount,newCount,zeroHash,operatorConfig,deployedAt,capturable
Chain names Ethereum / Arbitrum / Celo / Linea / Polygon / Optimism / Avalanche / Monad continue to fire. Already documented as intentional signal pointing to the aspirational Supported Networks table in index.mdx.
Other smaller error rules
Microsoft.Foreign(29): everye.g.,andi.e.,is an error. Either soften to warning, or full-pass replace with "for example" / "that is".Microsoft.Auto(11):auto-expires,auto-expiryhyphenation.proselint.Typography(9):...should be….write-good.ThereIs(8): "There is/are" sentence openers.Microsoft.Quotes(6): smart-quote style.Microsoft.Plurals(2):(es)parenthetical plurals.proselint.Cliches(1).
Recommendation
Before merging, address the Vale.Terms regression. Those 47 errors are net-new false positives that the seeded accept.txt introduces, including a few that point users at literal regex syntax (SDKs?, [Bb]igint). Everything else (em-dash sweep, vocab expansion for legit terms, deciding the fate of e.g. / i.e.) is fair game for follow-up PRs.
Pair with workspace commit c193202, which adds --filter='.Name != "Vale.Spelling"' to the PostToolUse hook command. Strict-in-CI / lenient-in-hook architecture per established practice (GitLab Vale docs, Vaadin .vale-pr.ini, ESLint warnings-as-anti- pattern). Spell-check at default error level in CI gates real typos before merge. The hook filters Vale.Spelling out of Claude's self-correction context so chain-name and identifier false positives don't bury Microsoft.Dashes / write-good rules. Why this beats the warning-tier compromise from 2823c5f: - Hook noise drops to ZERO instead of "less but still there". - CI signal stays at error tier where it belongs. - Single .vale.ini, no two-config sync footgun. - Native --filter, not a fragile grep post-process. A1igator's review pushed this in the right direction. Earlier compromise (Vale.Spelling = warning) reduced noise by one tier but didn't fix the architectural issue — wrong place to apply the lever. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Going to defer to you on whether to merge as-is or hold for further review, but here's the architectural cleanup your pushback drove. Pair of commits — workspace Workspace - vale "$f" 2>&1
+ vale --filter='.Name != "Vale.Spelling"' "$f" 2>&1PostToolUse hook now drops Vale.Spelling alerts from the JSON payload it injects into Claude's context. Uses Vale's native AST-level This PR Verified on
Why this beats the warning-tier compromise:
On the separate index.mdx finding: the Supported Networks table at References: GitLab Vale tiered convention, Vaadin .vale-pr.ini, Vale CLI --filter. Sorry for the back-and-forth — should've gone here in the first round instead of severity-tuning. Appreciate the push to actually think it through. |
|
The strict-in-CI / lenient-in-hook split is the right call, and the But this fix is orthogonal to the Re-ran on The
These will show up as errors in CI on any PR touching those identifiers, including PRs that are correct. Three options, any combination:
(1) is the smallest diff and matches the "vocab is spelling-only" mental model the rest of the setup now implies. No back-and-forth needed, the architectural pivot was worth doing. Just wanted to flag that the Vale.Terms misfires would otherwise ship with this PR. |
Per A1igator's tree-wide review on f662841: accept.txt entries are dual-used by Vale as both spelling acceptance AND Vale.Terms canonical-case enforcement, the latter at error severity. That fires 47 false positives across docs, including: - SDKs? -> 'Use SDKs? instead of sdk' (regex ? leaks literally, 24x) - paymentInfo -> flags TS type PaymentInfo (10x) - Base -> flags base code identifier / chain key (8x, my seed) - [Bb]igint -> tells users to type the regex string - Hono -> flags hono npm package in import paths - PaymentInfoHash -> flags variable paymentInfoHash (2x) - async -> flags Async capitalization Vocab here was added for spelling acceptance only. Vale.Terms was an accidental side-effect, never an intended feature. Disabled. If we later want real terminology enforcement, the right path is a dedicated styles/x402r/Terms.yml (or similar) with hand-curated canonical forms, not the spelling accept-list. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Good catch — and you were right that I'd previously framed those Verified tree-wide: Worth naming explicitly: my If we ever want real terminology enforcement (e.g. canonical Other items from your tree-wide audit that this PR doesn't address (all genuinely scope-out and worth follow-up PRs):
Should be clean now from this PR's perspective. Re-review when you have a minute. |
|
Did a config audit against Vale's official docs. One real bug, one mild tradeoff worth a comment. Bug —
|
Per the config audit comment on f662841: Microsoft.SentenceLength and Microsoft.Passive were set to `suggestion` while MinAlertLevel = warning, meaning Vale filters them out of output entirely. Same bug class as the Vale.Spelling regression caught earlier in this thread — `suggestion` looks like "downgrade" but is actually "off" under our floor. Original intent (per the rule comment header) was to silence these. Now matches that intent explicitly: - Microsoft.Contractions = NO (already was, explicit) - Microsoft.SentenceLength = NO (was: silently off via suggestion) - Microsoft.Passive = NO (was: silently off via suggestion) Why each is off: - Contractions: irrelevant for technical docs. - SentenceLength: low-ROI, no actionable threshold. - Passive: write-good.Passive already covers it with better signal. Also documented the [formats] mdx = md tradeoff per Vale's MDX docs (https://vale.sh/docs/formats/mdx): this is an extension substitution, not real MDX support. JSX components, JSX expressions, and ESM imports can leak into prose linting. Hook view (md mode) is strictly less accurate than CI view (mdx2vast). Acceptable tradeoff: zero-dep local install, with CI as the merge gate. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Fair catch — and it's the same bug class as the Vale.Spelling regression earlier in this thread, which makes the miss embarrassing on my part. I never re-audited the existing Microsoft rules through that lens after learning it the first time. Pushed 1. Silently-dead Microsoft rules. Changed both to explicit Picked 2. MDX format tradeoff. Added the limitation note to the Thanks for the audit. The |
|
Pulled 1. Exact match on every span — they're functionally identical on this corpus. If both were ever enabled, every passive sentence would double-fire. Dedup is correct. 2. Correction to my prior "silently dead" framing. I attributed the dead state of 3. Only
So this commit is tidying the config to make actual state explicit + correctly noting Microsoft.Passive is redundant with write-good.Passive. Not a silent-regression fix — but a clean cleanup with one real empirical insight (the 126/126 dedup). LGTM. |
Previous comment block on 1969bad attributed the dead state of Microsoft.SentenceLength and Microsoft.Passive to the same MinAlertLevel trap as the Vale.Spelling regression. That's wrong — both rules ship with `level: suggestion` in their YAML files (verified styles/Microsoft/*.yml), so they were already filtered by the warning floor regardless of any .vale.ini override. The old `= suggestion` lines were redundant restatements of defaults, not severity downgrades. Only Microsoft.Contractions (default `level: error`) had its behavior actually changed by `= NO` — that line suppresses ~37 hits across docs. Updated comment block to teach the actual mechanism per-rule. Also added a "footgun to watch for" note: the trap is real (a default-error rule downgraded to suggestion goes silent under warning floor), but only fires when the rule's default is error. Pure documentation fix. End-state unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Pushed
Comment now teaches the per-rule mechanism and includes a "footgun to watch for" note: the Thanks for the audit + the LGTM. Three iterations on this is more than I'd planned but the comment in the file is now accurate enough that the next person (or me, six months from now) won't repeat the mistake. |
Summary
Vale + project-memory layer for x402r docs writing. The companion PostToolUse hook and
writing-docsskill live in workspace.claude/— already onBackTrackCo/x402r-workspacemain(workspace hook commitd5acf69, skill rebuildff1eb65).AllowsYouTorule (stockproselint+write-goodpacks cover the same patterns)FirstPersonPluralrule (stockMicrosoft.Werule covers the same we/our/let's surface — keeping both produces double warnings).vale.iniparses.mdxas Markdown so local Vale runs without the separatemdx2vastbinary (CI'serrata-ai/vale-actionis unaffected — it installs its own MDX parser server-side)CLAUDE.mdto project context + hard constraints + pointer to the workspace-levelwriting-docsskillWhy
Voice inconsistency and AI slop were surviving the PR-only Vale check. Pairing this PR with the workspace PostToolUse hook gives Claude immediate Vale feedback while editing — violations surface as additional context for the next turn, no waiting for CI. Custom rules removed because stock packs already cover the same surface.
Test plan
brew install valecd docs && vale syncto download Microsoft/proselint/write-good packs (one-time).mdxfile withwe/our/let'sand confirm Vale output appears in the next turn (requires workspace hook commitd5acf69already on workspace main).mdxfile (e.g.docs.json) and confirm Vale doesn't runvale path/to/any.mdxlocally to confirm rule set:Slop,Enthusiasm,Microsoft.Weall firing on relevant patterns;AllowsYouToandFirstPersonPluralremovederrata-ai/vale-action) still runs on PR🤖 Generated with Claude Code