Skip to content

Commit c63dacb

Browse files
committed
feat(scaffold): block-prose-punctuation hook ships in every scaffolded app
Mirrors the framework-repo prose hook into scaffolded apps so end users get the same enforcement of AGENTS.md invariant 11 (no em-dashes, no hyphen-as-pause, no semicolon-as-pause, no xyz():-then-prose). The rule is already documented in templates/AGENTS.md and the per-agent rule files; the hook adds programmatic enforcement for Claude Code users. Three pieces: * templates/.claude/hooks/block-prose-punctuation.sh: copy of the framework hook * templates/.claude/settings.json: registers it on PreToolUse for Write/Edit/MultiEdit/NotebookEdit/Bash * lib/create.js: extends templateFiles copy list and chmod loop
1 parent cab76e7 commit c63dacb

3 files changed

Lines changed: 247 additions & 1 deletion

File tree

packages/cli/lib/create.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
266266
// Claude Code config + hooks
267267
'.claude.json',
268268
'.claude/settings.json',
269+
'.claude/hooks/block-prose-punctuation.sh',
269270
'.claude/hooks/guard-main-merge.sh',
270271
'.claude/hooks/guard-branch-context.sh',
271272
'.claude/hooks/nudge-uncommitted.sh',
@@ -294,7 +295,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
294295

295296
// Make hook scripts executable
296297
const { chmod } = await import('node:fs/promises');
297-
for (const hook of ['guard-main-merge.sh', 'guard-branch-context.sh', 'nudge-uncommitted.sh']) {
298+
for (const hook of ['block-prose-punctuation.sh', 'guard-main-merge.sh', 'guard-branch-context.sh', 'nudge-uncommitted.sh']) {
298299
const hookPath = join(appDir, '.claude', 'hooks', hook);
299300
if (existsSync(hookPath)) await chmod(hookPath, 0o755);
300301
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#!/usr/bin/env bash
2+
#
3+
# PreToolUse hook: block prose-punctuation patterns the webjs convention bans.
4+
#
5+
# Catches four classes of new content in tool calls:
6+
#
7+
# 1. U+2014 em-dash, anywhere.
8+
# 2. Space-hyphen-space " - " in PROSE contexts (comment lines, markdown
9+
# lines, headings, blockquotes). Math expressions in code like
10+
# `Math.abs(a - b)` or `arr.length - 1` are NOT flagged.
11+
# 3. Space-semicolon-space " ; " in PROSE contexts. JS / CSS statement
12+
# terminators (`;\n`) are NOT flagged.
13+
# 4. Code-shaped left-hand side immediately followed by a colon and prose:
14+
# - `<code>foo()</code>:` (markdown code-LHS in docs)
15+
# - `<my-tag>:` (custom-element tag with hyphen)
16+
# - Inline comment `// foo(): description`
17+
#
18+
# Why this exists: see AGENTS.md "Invariants", item 10. These patterns
19+
# confuse AI agents that try to parse the prose as TypeScript / shorthand-
20+
# method / object-literal syntax, and trip humans reading API docs.
21+
#
22+
# Covers two tool-call paths:
23+
# * Write / Edit / MultiEdit / NotebookEdit. The hook inspects the NEW
24+
# content fields of the tool payload. Existing glyphs in old_string
25+
# are not flagged: you can still Edit a line that contains one to
26+
# remove it.
27+
# * Bash. The hook inspects the command string, which catches commit
28+
# messages (`git commit -m "..."`), heredocs, echo / printf, and any
29+
# other prose typed at the shell.
30+
31+
set -euo pipefail
32+
33+
payload=$(cat)
34+
35+
# Pull every field where prose might land. `// empty` keeps missing
36+
# fields silent; `[]?` keeps array iteration safe when absent.
37+
new_content=$(printf '%s' "$payload" | jq -r '
38+
(.tool_input.content // empty),
39+
(.tool_input.new_string // empty),
40+
(.tool_input.new_source // empty),
41+
(.tool_input.command // empty),
42+
(.tool_input.edits[]?.new_string // empty)
43+
' 2>/dev/null || true)
44+
45+
if [ -z "$new_content" ]; then
46+
exit 0
47+
fi
48+
49+
# --- 1. U+2014 em-dash --------------------------------------------------
50+
if printf '%s' "$new_content" | grep -q $'\xe2\x80\x94'; then
51+
cat >&2 <<'EOF'
52+
BLOCKED: em-dash (U+2014) detected in this tool call.
53+
54+
webjs bans em-dashes repo-wide. Replace every U+2014 character with
55+
a period, comma, colon (on a plain-noun LHS), parentheses, or
56+
restructured sentence. Do NOT replace it with " - " or " ; " or a
57+
trailing colon on code: those are also banned. See rule 2 / 3 / 4
58+
below for the alternatives.
59+
60+
Rule: AGENTS.md, Invariants section, item 10.
61+
Hook: .claude/hooks/block-prose-punctuation.sh.
62+
EOF
63+
exit 2
64+
fi
65+
66+
# --- 2. Pause-hyphen " - " in PROSE contexts ----------------------------
67+
# Only flag lines whose context is clearly prose:
68+
# - Markdown lines starting with `#`, `>`, `*`, plain text outside code
69+
# fences (heuristic: line has no `=`, `{`, or `(...)` math)
70+
# - JSDoc / block comment lines starting with `*`
71+
# - Single-line comments starting with `//`
72+
#
73+
# Math expressions like `Math.abs(a - b)` or `arr.length - 1` are NOT
74+
# flagged because they appear in code lines (not comments) with code
75+
# context. The hook trades some false negatives in prose for zero false
76+
# positives in code-heavy diffs.
77+
78+
block_pause_hyphen=0
79+
80+
# Comment-line " - " pause: line starts with `//` or ` *` (JSDoc/block) or
81+
# `*` (markdown bold-start would have a letter after, distinguishable),
82+
# followed by prose with `\w+ - \w+` pattern. Specifically: catch lines
83+
# like `// foo - bar`, ` * foo - bar`, `* foo - bar`.
84+
if printf '%s\n' "$new_content" | grep -qE '^[[:space:]]*(//|\*)[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]'; then
85+
block_pause_hyphen=1
86+
fi
87+
88+
# Markdown heading " - " pause: line starts with `#` followed by prose
89+
# and ` - ` pattern.
90+
if printf '%s\n' "$new_content" | grep -qE '^#{1,6}[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]'; then
91+
block_pause_hyphen=1
92+
fi
93+
94+
# Markdown blockquote " - " pause: line starts with `>` followed by prose
95+
# and ` - ` pattern. (Single `>` blockquote, not table.)
96+
if printf '%s\n' "$new_content" | grep -qE '^>[[:space:]].*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]'; then
97+
block_pause_hyphen=1
98+
fi
99+
100+
# HTML / markdown <p>, <li>, <td> body " - " pause: line contains a
101+
# closing HTML tag from a prose context, then prose-style ` - `.
102+
if printf '%s\n' "$new_content" | grep -qE '<(p|li|td|h[1-6]|strong|em|blockquote)[^>]*>[^<]*[A-Za-z`)>][[:space:]]-[[:space:]][A-Za-z`(<]'; then
103+
block_pause_hyphen=1
104+
fi
105+
106+
if [ "$block_pause_hyphen" = "1" ]; then
107+
cat >&2 <<'EOF'
108+
BLOCKED: pause-hyphen " - " detected in a prose context.
109+
110+
webjs bans plain hyphens used as pause-punctuation in prose. Rewrite
111+
the sentence with a period, comma, colon (on a plain-noun LHS), or
112+
restructured phrasing.
113+
114+
Bad: // Foo - bar
115+
Good: // Foo, with bar
116+
Good: // Foo. Bar.
117+
118+
Bad: <li>Foo - bar.</li>
119+
Good: <li>Foo, with bar.</li>
120+
121+
Plain hyphens are still fine in compound words (`AI-first`), CLI
122+
flags (`--http2`), filenames, ranges, and math expressions in code
123+
(`arr.length - 1`, `Math.abs(a - b)`). The hook only flags the
124+
` < word > - < word > ` pause-pattern in prose contexts (comments,
125+
markdown headings, blockquotes, HTML prose tags).
126+
127+
Rule: AGENTS.md, Invariants section, item 10.
128+
Hook: .claude/hooks/block-prose-punctuation.sh.
129+
EOF
130+
exit 2
131+
fi
132+
133+
# --- 3. Pause-semicolon " ; " in PROSE contexts -------------------------
134+
# Same prose-context guard as #2.
135+
block_pause_semicolon=0
136+
137+
if printf '%s\n' "$new_content" | grep -qE '^[[:space:]]*(//|\*)[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]'; then
138+
block_pause_semicolon=1
139+
fi
140+
141+
if printf '%s\n' "$new_content" | grep -qE '^#{1,6}[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]'; then
142+
block_pause_semicolon=1
143+
fi
144+
145+
if printf '%s\n' "$new_content" | grep -qE '^>[[:space:]].*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]'; then
146+
block_pause_semicolon=1
147+
fi
148+
149+
if printf '%s\n' "$new_content" | grep -qE '<(p|li|td|h[1-6]|strong|em|blockquote)[^>]*>[^<]*[A-Za-z`)][[:space:]];[[:space:]][A-Za-z`(]'; then
150+
block_pause_semicolon=1
151+
fi
152+
153+
if [ "$block_pause_semicolon" = "1" ]; then
154+
cat >&2 <<'EOF'
155+
BLOCKED: pause-semicolon " ; " detected in a prose context.
156+
157+
webjs bans semicolons used as pause-punctuation in prose. Rewrite as
158+
two sentences (period) or with a conjunction (", and", ", but", ", so").
159+
160+
Bad: // Forms work ; links work too.
161+
Good: // Forms work. Links work too.
162+
Good: // Forms work, and links work too.
163+
164+
Semicolons stay fine inside code (JS statement terminators, CSS
165+
declarations) since those are not flagged.
166+
167+
Rule: AGENTS.md, Invariants section, item 10.
168+
Hook: .claude/hooks/block-prose-punctuation.sh.
169+
EOF
170+
exit 2
171+
fi
172+
173+
# --- 4a. <code>foo()</code>: prose ---------------------------------------
174+
# Markdown / HTML definition list with code-call followed by colon and
175+
# lowercase prose. The `)</code>:` shape is unambiguous: this is markdown,
176+
# not code, AND the inner code ends in `()` so the colon visually parses
177+
# as a return-type annotation.
178+
if printf '%s' "$new_content" | grep -qE '\)</code>:[[:space:]][a-z]'; then
179+
cat >&2 <<'EOF'
180+
BLOCKED: code-LHS colon-then-prose detected ("<code>foo()</code>: ...").
181+
182+
webjs bans `<code>foo()</code>: <prose>` because the colon visually
183+
parses as a TypeScript return-type annotation. Rewrite verb-led.
184+
185+
Bad: <code>repeat()</code>: keyed list directive
186+
Good: <code>repeat()</code> is the keyed list directive
187+
Good: <code>startServer()</code> creates an HTTP(S) server
188+
189+
Rule: AGENTS.md, Invariants section, item 10.
190+
Hook: .claude/hooks/block-prose-punctuation.sh.
191+
EOF
192+
exit 2
193+
fi
194+
195+
# --- 4b. Custom-element-tag <my-tag>: prose ------------------------------
196+
# HTML reserves hyphenated tag names for custom elements (W3C spec), so
197+
# `<x-y>:` is unambiguous prose, never JSX / TS / CSS.
198+
if printf '%s' "$new_content" | grep -qE '<[a-z][a-z0-9]*(-[a-z0-9]+)+([[:space:]][^>]*)?>:[[:space:]][a-z]'; then
199+
cat >&2 <<'EOF'
200+
BLOCKED: custom-element-tag colon-then-prose detected ("<my-tag>: ...").
201+
202+
webjs bans `<my-tag>: <prose>` in comments and docs. Rewrite verb-led.
203+
204+
Bad: // <ui-dialog>: owns open state, focus trap, escape, scroll lock.
205+
Good: // <ui-dialog> owns open state, focus trap, escape, scroll lock.
206+
Bad: // <ui-dialog-content>: the centered panel.
207+
Good: // <ui-dialog-content> is the centered panel.
208+
209+
Rule: AGENTS.md, Invariants section, item 10.
210+
Hook: .claude/hooks/block-prose-punctuation.sh.
211+
EOF
212+
exit 2
213+
fi
214+
215+
# --- 4c. Inline / JSDoc comment "foo(): prose" --------------------------
216+
# Match comment-line prefix (`//` or leading `*`) before `\w+(...): ` and
217+
# lowercase prose. Avoids TS return-type annotations because those never
218+
# appear inside comment lines.
219+
if printf '%s\n' "$new_content" | grep -qE '^[[:space:]]*(//|\*)[[:space:]][^(]*[A-Za-z_][A-Za-z0-9_]*\([^)]*\):[[:space:]][a-z]'; then
220+
cat >&2 <<'EOF'
221+
BLOCKED: comment-line code-LHS colon-then-prose detected ("// foo(): ...").
222+
223+
webjs bans `xyz(): <prose>` inside comments and JSDoc. Rewrite verb-led.
224+
225+
Bad: // firstUpdated(): once, on the first render only
226+
Good: // firstUpdated() runs once, on the first render only
227+
Bad: // closest(): null if the click wasn't inside a frame
228+
Good: // closest() returns null when the click wasn't inside a frame
229+
230+
Rule: AGENTS.md, Invariants section, item 10.
231+
Hook: .claude/hooks/block-prose-punctuation.sh.
232+
EOF
233+
exit 2
234+
fi
235+
236+
exit 0

packages/cli/templates/.claude/settings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
{
22
"hooks": {
33
"PreToolUse": [
4+
{
5+
"matcher": "Write|Edit|MultiEdit|NotebookEdit|Bash",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": ".claude/hooks/block-prose-punctuation.sh"
10+
}
11+
]
12+
},
413
{
514
"matcher": "Bash",
615
"hooks": [

0 commit comments

Comments
 (0)