You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
.claude/hooks/block-prose-punctuation.sh gates its two pause-punctuation rules on a PROSE-CONTEXT heuristic that only recognises comment lines (//, *), markdown headings (#), and blockquotes (>). A JSON string value matches none of those, so invariant 11 violations pass straight through in .json files.
That is not a theoretical surface. packages/ui/package.jsondescription is the blurb npm renders on the registry page and in npm search results, so it is some of the most-read prose the project ships. During #1129 (PR #1235) a space-surrounded hyphen used as a pause landed in exactly that field, the hook allowed it, and a reviewer caught it instead.
Measured against the hook as it stands today, per rule:
Rule
JSON string value
Same prose in markdown
1, em-dash (U+2014)
blocked
blocked
2, pause-hyphen -
allowed
blocked
3, pause-semicolon ;
allowed
blocked
5, lowercase webjs brand
blocked
blocked
So the gap is exactly rules 2 and 3, the two that are prose-context-gated. Rules 1 and 5 are blunt (they match anywhere) and already cover JSON.
Reproduce with the hook directly:
printf'{"tool_name":"Write","tool_input":{"file_path":"/x/package.json","content":" \\"description\\": \\"A library - for things\\","}}' \
| bash .claude/hooks/block-prose-punctuation.sh;echo"exit=$?"# 0, allowedprintf'{"tool_name":"Write","tool_input":{"file_path":"/x/README.md","content":"# A library - for things"}}' \
| bash .claude/hooks/block-prose-punctuation.sh;echo"exit=$?"# 2, blocked
Implementation plan
Decision: Give rules 2 and 3 a fifth prose-context pattern each, matching a JSON string ASSIGNMENT whose KEY is on a closed three-name prose list, and reusing each rule's existing character-class core byte for byte. The scope rule is the KEY, not the file and not the value: a line shaped ^\s*"(description|title|displayName)"\s*:\s*"<value>" has its value scanned for the same pause cores the other four contexts already use, and every other key is left alone. Those three are exactly the prose-bearing keys this repo's tracked JSON uses today (59 description, 5 title, 1 displayName); name is deliberately excluded because its 127 occurrences are package, component, and snippet identifiers, which makes it the single largest false-positive source in the tree. Matching is FILE-PATH AGNOSTIC by design, on line shape alone, because the Bash payload carries no file_path at all, so a heredoc or a jq write into package.json would walk straight through a file-gated rule, and because the four existing context patterns are all content-shaped, so a key-shaped fifth extends the mechanism instead of adding a second one. Keying on the key is also what bounds the risk: every false-positive candidate lives under a different key, so all of them are excluded for free. Measured over every tracked .json file in the repo, the two patterns produce exactly 2 hits, both real invariant 11 violations, and 0 false positives.
Explicit ruling per candidate:
JSON value
Scanned?
Why
"description"
YES
The npm registry blurb and the JSON Schema field docs. The most-read prose the project ships.
"title"
YES
JSON Schema title (webjs-config.schema.json).
"displayName"
YES
The VS Code Marketplace listing name.
"name"
no
A package / component / snippet identifier, never prose. 127 occurrences.
A "webjs" config block value
no
A config leaf ("basePath": "/app - v2") is a value, not prose. Its own description entries in webjs-config.schema.json ARE scanned, because that leaf key is description.
A "scripts" command
no
A shell command is code. A hyphen there is stdin, a flag, or an argument.
A dependency version range
no
Excluded twice over: the key is not on the list, and npm's hyphen range ("1.2.3 - 2.3.4") is digit-bounded, so the shared [A-Za-z...] core could never match it even if it were.
A URL
no
Keys are homepage / repository / url, and a URL cannot carry a raw space, so the pause shape is unmatchable.
A file path
no
Keys are main / bin / exports / files.
A glob
no
Keys are files / include / exclude, and array elements have no key at all.
Rejected:
Scanning every JSON string value bluntly, like rule 1. It meets semver ranges and script commands, and abandons the trade the file's own header commits to ("zero false positives in code-heavy diffs").
Gating on file_path ending in .json, or on a file allowlist (package.json, *.schema.json, manifest.json). The Bash path exposes only .tool_input.command, so every heredoc write escapes it, and it introduces a second mechanism alongside four content-shaped patterns.
Scanning any JSON key/value line regardless of key. "name" alone would put 127 identifier values behind a blocking gate.
Adding speculative prose keys (summary, short_name, detail). None exists anywhere in the tree, so each is unbacked false-positive surface. A fourth key is a one-word edit when a real surface appears.
Leaning on test/repo-health/published-package-descriptions.test.mjs (added by feat: make npm descriptions definitional and add a sameAs graph #1248) instead of fixing the hook. It asserts the same invariant, but only over the 8 non-private published manifests, and only at CI time. Both live violations below sit in files it skips.
Steps
.claude/hooks/block-prose-punctuation.sh, rule 2 (banner L70, flag block_pause_hyphen set at L82). After the HTML-prose-tag pattern at L106, add a fifth context:
# JSON prose-value " - " pause: a string assignment whose KEY is one of the# three prose-bearing keys this project's JSON uses. Scoping to the key is# what keeps this off semver ranges, script commands, urls, paths and globs,# every one of which lives under a different key. Shape, not file path: the# Bash payload has no file_path, so a heredoc writing a manifest is covered.ifprintf'%s\n'"$new_content"| grep -qE '^[[:space:]]*"(description|title|displayName)"[[:space:]]*:[[:space:]]*".*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]';then
block_pause_hyphen=1
fi
Same file, rule 3 (banner L137, flag block_pause_semicolon set at L139). After the HTML-prose-tag pattern at L153, add the mirror, whose core is [A-Za-z)]/[A-Za-z(] with no angle brackets, matching L141 exactly:
ifprintf'%s\n'"$new_content"| grep -qE '^[[:space:]]*"(description|title|displayName)"[[:space:]]*:[[:space:]]*".*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]';then
block_pause_semicolon=1
fi
Same file, the two block messages (the hyphen heredoc at L110-135, the semicolon heredoc at L157-175). Add one JSON example line to each Bad/Good pair, and correct the citation footer in both from AGENTS.md, Invariants section, item 10 to item 11. Item 10 is erasable TypeScript; rule 5's footer at L319 already cites 11 correctly, so these two are stale.
Same file, the header comment at L8-12, which enumerates the prose contexts as "comment lines, markdown lines, headings, blockquotes". Add the JSON prose-key context and name the three keys, so the file states its own scope.
Mirror steps 1 through 4 into the two other copies of the hook: packages/cli/templates/.claude/hooks/block-prose-punctuation.sh (ships into every scaffolded app) and examples/blog/.claude/hooks/block-prose-punctuation.sh. Those two are byte-identical to each other and are the repo copy MINUS rule 5, so rules 1 through 4 must stay in step across all three. Nothing guards this today, so it is a manual edit that step 6 of the Tests list then pins.
Fix the two violations the new rule makes visible, in the same change, since the hook would otherwise block the next edit to either line:
package.json:5, "WebJs - AI-first, web-components-first framework." becomes "WebJs is an AI-first, web-components-first framework."
packages/ui/packages/registry/package.json:6, "Source registry for @webjsdev/ui - component sources, themes, lib. ..." becomes "Source registry for @webjsdev/ui, holding component sources, themes, and lib. ..."
Tests
All in test/hooks/block-prose-punctuation.test.mjs (22 tests today, driven through runContent(), which spawns the hook with a real {tool_input:{content}} payload). Note the file currently has NO rule 2 or rule 3 coverage at all, only em-dash, brand, and the CLI drift guard, so the markdown "must stay blocked" assertions are new too.
Unit, blocks: a "description" line whose value carries a pause-hyphen exits 2, and the stderr matches /pause-hyphen/. Same for a pause-semicolon value against /pause-semicolon/. Both are asserted at a deep indentation as well, since webjs-config.schema.json nests description twelve spaces in.
Unit, key coverage: "title" and "displayName" block on the same value, and the exact pre-fix string from package.json:5 blocks.
Unit, false positives that must stay ALLOWED (exit 0), one assertion each: an npm hyphen version range ("drizzle-orm": "1.2.3 - 2.3.4"), an "engines" range, a "scripts" value containing a bare - argument, a "name" value with a hyphen, a "webjs" config leaf value ("basePath": "/app - v2"), a "main" path, and a "description" whose only hyphens are compound words ("An AI-first, web-components-first framework."). The compound-word case is the one that proves the shared character-class core was reused rather than reinvented.
Unit, markdown unchanged: the existing heading, blockquote, and comment-line contexts still block, so the fifth pattern did not disturb the first four.
Repo drift guard, in the same file: walk every tracked *.json, feed each prose-key line through the hook, and assert exit 0. This is what stops the two lines fixed in step 6 from coming back, and it reds today against the unfixed tree.
Copy drift guard, in the same file: assert packages/cli/templates/.claude/hooks/... and examples/blog/.claude/hooks/... are byte-identical to each other, and that the repo copy contains both new grep -qE lines that those copies carry. Without it, step 5 can be half-applied and every scaffolded app ships a hook missing the rule.
Counterfactual, on reverting the fix alone: delete just the two grep -qE blocks from steps 1 and 2 and every new blocking test flips from exit 2 to exit 0, while the allow-list tests stay green (they were already green, which is the point of listing them). Revert only step 5 and the copy drift guard reds while the behaviour tests stay green, which is what distinguishes the two failure modes.
Doc surfaces
AGENTS.md, invariant 11 at L473. The clause "semicolons and colons stay fine inside code / TS / JSON / CSS" currently reads as a blanket JSON exemption and is now wrong for the three prose keys. Narrow it to say JSON SYNTAX is code while a description / title / displayName VALUE is prose and is scanned.
Nothing else. The scaffold and dogfood rule copies (packages/cli/templates/.agents/rules/workflow.md, examples/blog/.agents/rules/workflow.md, examples/blog/.cursorrules, examples/blog/.github/copilot-instructions.md) state the rule generically with no JSON exemption, so they are already correct. No docs site, website, README, or skill surface applies: this is repo tooling, not framework behaviour.
Implementation notes (for the implementing agent)
Verified behaviour, measured against the current checkout (clean at main, aaf0c568). Each row was run through the real payload path, not a bare pipe:
Input, as a Write payload
Exit
Rule that fired
"description": "WebJs - AI-first, ..." (the live package.json:5)
0
none, ALLOWED
"description": "Forms work ; links work too."
0
none, ALLOWED
"description": "A library <em-dash> for things"
2
rule 1
"description": "... for webjs apps."
2
rule 5
# A library - for things (same prose, markdown)
2
rule 2
"node": ">=24.0.0-alpha - 25"
0
none, correctly allowed
Isolating rule 2 matters: the pre-#1248 strings ("webjs CLI - dev, start, create, db") DO exit 2, but on the brand rule, not the pause rule. Capitalize the brand in that same string and it drops to exit 0, which is the clean demonstration of the gap.
Did a banned glyph actually ship in #1248? No. #1248 (175bf443, merged today) REMOVED two pause-hyphens ("webjs core runtime - html/css tags, ...", "webjs CLI - dev, start, create, db") and added test/repo-health/published-package-descriptions.test.mjs to guard the surface. The evidence is what that guard does not reach: it derives its list from non-private manifests under packages, packages/editors, and packages/wrappers, which is 8 packages, so it skips the repo root, the nested packages/ui/packages/*, and every private: true editor manifest. Both surviving violations sit in exactly those blind spots, and both are live on main right now:
packages/ui/packages/registry/package.json:6, "Source registry for @webjsdev/ui - component sources, themes, lib."
Those two are the whole output of the proposed pattern over every tracked .json file, which is the false-positive measurement as well as the bug report.
Where to edit:
.claude/hooks/block-prose-punctuation.sh:
Rule 2 starts at the # --- 2. Pause-hyphen banner (L70), zeroes block_pause_hyphen at L82, and sets it from FOUR grep -qE calls, not three: the comment-line pattern (L88), the markdown-heading pattern (L94), the blockquote pattern (L100), and the HTML-prose-tag pattern (L106). Add a fifth after L106.
Rule 3 starts at # --- 3. Pause-semicolon (L137), zeroes its flag at L139, and mirrors the same four patterns at L141, L145, L149, L153. Add the matching JSON pattern after L153.
Do NOT touch rule 1 (L53-68) or rule 5 (L240-324): both already fire on JSON, verified above.
Input extraction is at L41-47: the hook reads .tool_input.content, .new_string, .new_source, .command, and .edits[]?.new_string. Every path that can write a package.json therefore already reaches the rule bodies, so nothing needs adding there.
Landmines / gotchas:
The hook is a PreToolUse gate: exit 2 BLOCKS the write. A false positive is not a warning, it stops work, which is why the existing rules are deliberately conservative. Prefer under-matching over over-matching.
THREE copies of this hook exist. .claude/hooks/ is the repo copy; packages/cli/templates/.claude/hooks/ and examples/blog/.claude/hooks/ are byte-identical to each other and are the repo copy minus rule 5 (a scaffolded app has no WebJs brand obligation). Rules 1 through 4 are byte-identical across all three, no drift guard exists, and the scaffold copy reaches every app webjs create generates. Edit all three.
The four existing context patterns each wrap the pause in a character class, [A-Za-z)>]before and[A-Za-z(<] after for the hyphen, [A-Za-z)]/[A-Za-z(] for the semicolon. Reuse the core verbatim so the JSON pattern agrees with the others about what counts as a pause between words. It is also load-bearing: the letter bound is what makes "1.2.3 - 2.3.4" unmatchable, so a loosened class silently admits every semver hyphen range in the tree.
This hook has ALREADY been narrowed once for false positives, see dogfood: prose hook false-positives on 'webjs <subcmd>' before a closing quote #956 (dogfood: prose hook false-positives on 'webjs <subcmd>' before a closing quote), where the brand rule's trailing character class had to admit a closing quote so a package.json line stopped tripping it. Read that fix before widening anything: the same file, the same class of over-match.
Escaping: a JSON value inside the payload is itself JSON-escaped by the time the hook sees it. Test through the real payload shape, not by piping a bare line, or the pattern will look right and not fire in practice. jq -Rs '{tool_input:{content:.}}' is the least error-prone way to build one.
Editing packages/server/webjs-config.schema.json trips rule 5 on its existing "title": "webjs config block" line, since config is not a CLI subcommand. That is a pre-existing rule 5 over-match, out of scope here, and worth knowing before an unrelated edit to that file is blamed on this change.
The hook's rule 2 and rule 3 messages cite Invariants section, item 10, which is the erasable-TypeScript invariant. The prose rule is item 11, as rule 5's own message says. Fix the two footers while editing those blocks.
Invariants to respect:
Root AGENTS.md invariant 11 is the rule being enforced. This issue changes the surfaces on which it is DETECTED, and narrows one over-broad sentence in its wording, not the rule itself.
The hook is committed in-repo under .claude/hooks/ so a fresh clone carries it; keep it dependency-free shell plus jq, as it is today.
Acceptance criteria
A - pause in a description / title / displayName JSON value is blocked, and the same line in markdown stays blocked
A ; pause in the same three JSON values is blocked
A hyphen under any other key is still allowed, proven per candidate: an npm version range, an "engines" range, a "scripts" command, a "name", a "webjs" config leaf, a "main" path
A compound word inside a scanned description ("An AI-first, web-components-first framework.") is still allowed
Rules 1 and 5 are unchanged, since both already cover JSON
The pre-fix package.json:5 string is blocked by the new rule, and both live violations are fixed in the same change
Every tracked *.json file passes the hook, asserted by a test that reds against the unfixed tree
All three copies of the hook carry the new patterns, asserted by a test
AGENTS.md invariant 11 no longer claims a blanket JSON exemption
A counterfactual proves each new test actually fires (remove the pattern, test reds)
Problem
.claude/hooks/block-prose-punctuation.shgates its two pause-punctuation rules on a PROSE-CONTEXT heuristic that only recognises comment lines (//,*), markdown headings (#), and blockquotes (>). A JSON string value matches none of those, so invariant 11 violations pass straight through in.jsonfiles.That is not a theoretical surface.
packages/ui/package.jsondescriptionis the blurb npm renders on the registry page and innpm searchresults, so it is some of the most-read prose the project ships. During #1129 (PR #1235) a space-surrounded hyphen used as a pause landed in exactly that field, the hook allowed it, and a reviewer caught it instead.Measured against the hook as it stands today, per rule:
-;webjsbrandSo the gap is exactly rules 2 and 3, the two that are prose-context-gated. Rules 1 and 5 are blunt (they match anywhere) and already cover JSON.
Reproduce with the hook directly:
Implementation plan
Decision: Give rules 2 and 3 a fifth prose-context pattern each, matching a JSON string ASSIGNMENT whose KEY is on a closed three-name prose list, and reusing each rule's existing character-class core byte for byte. The scope rule is the KEY, not the file and not the value: a line shaped
^\s*"(description|title|displayName)"\s*:\s*"<value>"has its value scanned for the same pause cores the other four contexts already use, and every other key is left alone. Those three are exactly the prose-bearing keys this repo's tracked JSON uses today (59description, 5title, 1displayName);nameis deliberately excluded because its 127 occurrences are package, component, and snippet identifiers, which makes it the single largest false-positive source in the tree. Matching is FILE-PATH AGNOSTIC by design, on line shape alone, because theBashpayload carries nofile_pathat all, so a heredoc or ajqwrite intopackage.jsonwould walk straight through a file-gated rule, and because the four existing context patterns are all content-shaped, so a key-shaped fifth extends the mechanism instead of adding a second one. Keying on the key is also what bounds the risk: every false-positive candidate lives under a different key, so all of them are excluded for free. Measured over every tracked.jsonfile in the repo, the two patterns produce exactly 2 hits, both real invariant 11 violations, and 0 false positives.Explicit ruling per candidate:
"description""title"webjs-config.schema.json)."displayName""name""webjs"config block value"basePath": "/app - v2") is a value, not prose. Its owndescriptionentries inwebjs-config.schema.jsonARE scanned, because that leaf key isdescription."scripts"command"1.2.3 - 2.3.4") is digit-bounded, so the shared[A-Za-z...]core could never match it even if it were.homepage/repository/url, and a URL cannot carry a raw space, so the pause shape is unmatchable.main/bin/exports/files.files/include/exclude, and array elements have no key at all.Rejected:
file_pathending in.json, or on a file allowlist (package.json,*.schema.json,manifest.json). TheBashpath exposes only.tool_input.command, so every heredoc write escapes it, and it introduces a second mechanism alongside four content-shaped patterns."name"alone would put 127 identifier values behind a blocking gate.summary,short_name,detail). None exists anywhere in the tree, so each is unbacked false-positive surface. A fourth key is a one-word edit when a real surface appears.test/repo-health/published-package-descriptions.test.mjs(added by feat: make npm descriptions definitional and add a sameAs graph #1248) instead of fixing the hook. It asserts the same invariant, but only over the 8 non-private published manifests, and only at CI time. Both live violations below sit in files it skips.Steps
.claude/hooks/block-prose-punctuation.sh, rule 2 (banner L70, flagblock_pause_hyphenset at L82). After the HTML-prose-tag pattern at L106, add a fifth context:Same file, rule 3 (banner L137, flag
block_pause_semicolonset at L139). After the HTML-prose-tag pattern at L153, add the mirror, whose core is[A-Za-z)]/[A-Za-z(]with no angle brackets, matching L141 exactly:Same file, the two block messages (the hyphen heredoc at L110-135, the semicolon heredoc at L157-175). Add one JSON example line to each Bad/Good pair, and correct the citation footer in both from
AGENTS.md, Invariants section, item 10toitem 11. Item 10 is erasable TypeScript; rule 5's footer at L319 already cites 11 correctly, so these two are stale.Same file, the header comment at L8-12, which enumerates the prose contexts as "comment lines, markdown lines, headings, blockquotes". Add the JSON prose-key context and name the three keys, so the file states its own scope.
Mirror steps 1 through 4 into the two other copies of the hook:
packages/cli/templates/.claude/hooks/block-prose-punctuation.sh(ships into every scaffolded app) andexamples/blog/.claude/hooks/block-prose-punctuation.sh. Those two are byte-identical to each other and are the repo copy MINUS rule 5, so rules 1 through 4 must stay in step across all three. Nothing guards this today, so it is a manual edit that step 6 of the Tests list then pins.Fix the two violations the new rule makes visible, in the same change, since the hook would otherwise block the next edit to either line:
package.json:5,"WebJs - AI-first, web-components-first framework."becomes"WebJs is an AI-first, web-components-first framework."packages/ui/packages/registry/package.json:6,"Source registry for @webjsdev/ui - component sources, themes, lib. ..."becomes"Source registry for @webjsdev/ui, holding component sources, themes, and lib. ..."Tests
All in
test/hooks/block-prose-punctuation.test.mjs(22 tests today, driven throughrunContent(), which spawns the hook with a real{tool_input:{content}}payload). Note the file currently has NO rule 2 or rule 3 coverage at all, only em-dash, brand, and the CLI drift guard, so the markdown "must stay blocked" assertions are new too."description"line whose value carries a pause-hyphen exits 2, and the stderr matches/pause-hyphen/. Same for a pause-semicolon value against/pause-semicolon/. Both are asserted at a deep indentation as well, sincewebjs-config.schema.jsonnestsdescriptiontwelve spaces in."title"and"displayName"block on the same value, and the exact pre-fix string frompackage.json:5blocks."drizzle-orm": "1.2.3 - 2.3.4"), an"engines"range, a"scripts"value containing a bare-argument, a"name"value with a hyphen, a"webjs"config leaf value ("basePath": "/app - v2"), a"main"path, and a"description"whose only hyphens are compound words ("An AI-first, web-components-first framework."). The compound-word case is the one that proves the shared character-class core was reused rather than reinvented.*.json, feed each prose-key line through the hook, and assert exit 0. This is what stops the two lines fixed in step 6 from coming back, and it reds today against the unfixed tree.packages/cli/templates/.claude/hooks/...andexamples/blog/.claude/hooks/...are byte-identical to each other, and that the repo copy contains both newgrep -qElines that those copies carry. Without it, step 5 can be half-applied and every scaffolded app ships a hook missing the rule.Counterfactual, on reverting the fix alone: delete just the two
grep -qEblocks from steps 1 and 2 and every new blocking test flips from exit 2 to exit 0, while the allow-list tests stay green (they were already green, which is the point of listing them). Revert only step 5 and the copy drift guard reds while the behaviour tests stay green, which is what distinguishes the two failure modes.Doc surfaces
AGENTS.md, invariant 11 at L473. The clause "semicolons and colons stay fine inside code / TS / JSON / CSS" currently reads as a blanket JSON exemption and is now wrong for the three prose keys. Narrow it to say JSON SYNTAX is code while adescription/title/displayNameVALUE is prose and is scanned.packages/cli/templates/.agents/rules/workflow.md,examples/blog/.agents/rules/workflow.md,examples/blog/.cursorrules,examples/blog/.github/copilot-instructions.md) state the rule generically with no JSON exemption, so they are already correct. No docs site, website, README, or skill surface applies: this is repo tooling, not framework behaviour.Implementation notes (for the implementing agent)
Verified behaviour, measured against the current checkout (clean at main,
aaf0c568). Each row was run through the real payload path, not a bare pipe:Writepayload"description": "WebJs - AI-first, ..."(the livepackage.json:5)"description": "Forms work ; links work too.""description": "A library <em-dash> for things""description": "... for webjs apps."# A library - for things(same prose, markdown)"node": ">=24.0.0-alpha - 25"Isolating rule 2 matters: the pre-#1248 strings (
"webjs CLI - dev, start, create, db") DO exit 2, but on the brand rule, not the pause rule. Capitalize the brand in that same string and it drops to exit 0, which is the clean demonstration of the gap.Did a banned glyph actually ship in #1248? No. #1248 (
175bf443, merged today) REMOVED two pause-hyphens ("webjs core runtime - html/css tags, ...","webjs CLI - dev, start, create, db") and addedtest/repo-health/published-package-descriptions.test.mjsto guard the surface. The evidence is what that guard does not reach: it derives its list from non-private manifests underpackages,packages/editors, andpackages/wrappers, which is 8 packages, so it skips the repo root, the nestedpackages/ui/packages/*, and everyprivate: trueeditor manifest. Both surviving violations sit in exactly those blind spots, and both are live on main right now:package.json:5,"WebJs - AI-first, web-components-first framework."packages/ui/packages/registry/package.json:6,"Source registry for @webjsdev/ui - component sources, themes, lib."Those two are the whole output of the proposed pattern over every tracked
.jsonfile, which is the false-positive measurement as well as the bug report.Where to edit:
.claude/hooks/block-prose-punctuation.sh:# --- 2. Pause-hyphenbanner (L70), zeroesblock_pause_hyphenat L82, and sets it from FOURgrep -qEcalls, not three: the comment-line pattern (L88), the markdown-heading pattern (L94), the blockquote pattern (L100), and the HTML-prose-tag pattern (L106). Add a fifth after L106.# --- 3. Pause-semicolon(L137), zeroes its flag at L139, and mirrors the same four patterns at L141, L145, L149, L153. Add the matching JSON pattern after L153..tool_input.content,.new_string,.new_source,.command, and.edits[]?.new_string. Every path that can write apackage.jsontherefore already reaches the rule bodies, so nothing needs adding there.Landmines / gotchas:
PreToolUsegate: exit 2 BLOCKS the write. A false positive is not a warning, it stops work, which is why the existing rules are deliberately conservative. Prefer under-matching over over-matching..claude/hooks/is the repo copy;packages/cli/templates/.claude/hooks/andexamples/blog/.claude/hooks/are byte-identical to each other and are the repo copy minus rule 5 (a scaffolded app has no WebJs brand obligation). Rules 1 through 4 are byte-identical across all three, no drift guard exists, and the scaffold copy reaches every appwebjs creategenerates. Edit all three.[A-Za-z)>]before and[A-Za-z(<]after for the hyphen,[A-Za-z)]/[A-Za-z(]for the semicolon. Reuse the core verbatim so the JSON pattern agrees with the others about what counts as a pause between words. It is also load-bearing: the letter bound is what makes"1.2.3 - 2.3.4"unmatchable, so a loosened class silently admits every semver hyphen range in the tree.dogfood: prose hook false-positives on 'webjs <subcmd>' before a closing quote), where the brand rule's trailing character class had to admit a closing quote so apackage.jsonline stopped tripping it. Read that fix before widening anything: the same file, the same class of over-match.jq -Rs '{tool_input:{content:.}}'is the least error-prone way to build one.packages/server/webjs-config.schema.jsontrips rule 5 on its existing"title": "webjs config block"line, sinceconfigis not a CLI subcommand. That is a pre-existing rule 5 over-match, out of scope here, and worth knowing before an unrelated edit to that file is blamed on this change.Invariants section, item 10, which is the erasable-TypeScript invariant. The prose rule is item 11, as rule 5's own message says. Fix the two footers while editing those blocks.Invariants to respect:
AGENTS.mdinvariant 11 is the rule being enforced. This issue changes the surfaces on which it is DETECTED, and narrows one over-broad sentence in its wording, not the rule itself..claude/hooks/so a fresh clone carries it; keep it dependency-free shell plusjq, as it is today.Acceptance criteria
-pause in adescription/title/displayNameJSON value is blocked, and the same line in markdown stays blocked;pause in the same three JSON values is blocked"engines"range, a"scripts"command, a"name", a"webjs"config leaf, a"main"pathdescription("An AI-first, web-components-first framework.") is still allowedpackage.json:5string is blocked by the new rule, and both live violations are fixed in the same change*.jsonfile passes the hook, asserted by a test that reds against the unfixed treeAGENTS.mdinvariant 11 no longer claims a blanket JSON exemptionnode --test test/hooks/block-prose-punctuation.test.mjspasses