Stops Claude from publishing claims nobody measured. Before a post, page or README is written or typed into a browser, it blocks words like 100%, guaranteed…

A Claude Code mod that stops Claude from publishing claims nobody measured.
I use Claude to write posts, store pages and video captions for my products. AI writing drifts toward big promises. In one day of review I found these in drafts that were about to go out:
Each one would have been read by customers as a promise. This mod stops the write before it happens and asks Claude for the measured fact instead.
/content/, /posts/, /blog/, /drafts/, README.md and more, all configurable), it looks for overclaiming words./overclaim-allow. The next stopped write passes, and only that one.Default words: 100%, guaranteed, guarantee, zero risk, never fails, always works, perfect, flawless, bulletproof, and Japanese 完全, ゼロ, 確実, 絶対, 必ず, 爆速. Matching ignores case.
/plugin marketplace add naoanao/overclaim-guard
/plugin install overclaim-guard@overclaim-guard
| Setting | Default | What it means |
|---|---|---|
| Words to stop | the list above | Comma separated. Replaces the list. |
| Where published writing lives | /content/, /posts/, /blog/, /drafts/, /newsletter/, /announcements/, /press/, /public/*.html, /site/*.md, README.md, CHANGELOG.md | Comma separated path fragments. * matches within one folder. |
| Check text typed in the browser | on | Also check posts typed or pasted through browser tools. |
/overclaim-allow.tool.call on Write, Edit, and browser tools that type, paste or fillsession.start and command.run, for /overclaim-allowclaude plugin test .
There are 9 tests: the rules on their own, plus the whole mod running in Claude Code's test engine. Each of three guards was broken once on purpose to confirm its test fails for the right reason:
Made by nao, an AI consultant who builds. I design and run AI automation for small businesses, from first conversation to production. Portfolio: https://growl-ai.com/portfolio/en
Also by me:
MIT License.
測っていない言い過ぎを、Claude が外に出す前に止める部品です。
/overclaim-allow を使います。止める語(最初の設定):完全・ゼロ・確実・絶対・必ず・爆速 と、英語の 100%・guaranteed など。外には何も送りません。
hooks/register.ts 63 lines1import type { Register, EngineInterface } from 'claude-code'
2import { DEFAULT_WORDS, DEFAULT_PATHS, list, isPublished, findOverclaims, denyMessage, typedText } from './logic.js'
3
4let words: string[] = DEFAULT_WORDS
5let paths: string[] = DEFAULT_PATHS
6let watchBrowser = true
7
8const ALLOW = 'allowNext'
9
10// The deny text, or undefined when the text may pass. A one-time allowance is used up here.
11async function verdict($: EngineInterface, where: string, text: string): Promise<string | undefined> {
12 const hits = findOverclaims(text, words)
13 if (hits.length === 0) return undefined
14 // When the store cannot be read, stop rather than let it through.
15 const allowed = await $.store.get(ALLOW).catch(() => undefined)
16 if (allowed === true) {
17 await $.store.delete(ALLOW).catch(() => undefined)
18 return undefined
19 }
20 return denyMessage(where, hits)
21}
22
23export const register: Register = (on, options) => {
24 const o = (options ?? {}) as { words?: string; paths?: string; watchBrowser?: boolean }
25 words = list(o.words, DEFAULT_WORDS)
26 paths = list(o.paths, DEFAULT_PATHS)
27 watchBrowser = o.watchBrowser !== false
28
29 on('session.start', async ($, e, next) => {
30 await $.command.register({
31 name: 'overclaim-allow',
32 description: 'Let the next overclaim through once (when the word is intended and the person agreed)',
33 })
34 return next(e)
35 })
36
37 on('command.run', { command: 'overclaim-allow' }, async $ => {
38 await $.store.set(ALLOW, true)
39 return { text: 'overclaim-guard: the next stopped write or post will pass once.' }
40 })
41
42 on('tool.call', { tool: 'Write' }, async ($, e, next) => {
43 if (!isPublished(e.file_path, paths)) return next(e)
44 const deny = await verdict($, e.file_path, e.content)
45 return deny ? { deny } : next(e)
46 }).catch(($, e, next) => next(e))
47
48 on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
49 if (!isPublished(e.file_path, paths)) return next(e)
50 const deny = await verdict($, e.file_path, e.new_string)
51 return deny ? { deny } : next(e)
52 }).catch(($, e, next) => next(e))
53
54 // Posts typed or pasted through a browser tool (X, YouTube, a blog editor).
55 on('tool.call', async ($, e, next) => {
56 if (!watchBrowser || !/^mcp__.+__(computer|javascript_tool|form_input)$/.test(String(e.tool))) return next(e)
57 const text = typedText(e.tool, e as unknown as Record<string, unknown>)
58 if (!text) return next(e)
59 const deny = await verdict($, 'text typed in the browser', text)
60 return deny ? { deny } : next(e)
61 }).catch(($, e, next) => next(e))
62}
63hooks/logic.js 71 lines1// Pure decision functions. No engine calls here, so the tests can call them directly.
2
3// Words that promise more than anyone measured. English and Japanese by default.
4export const DEFAULT_WORDS = [
5 '100%', 'guaranteed', 'guarantee', 'zero risk', 'never fails', 'always works', 'perfect', 'flawless', 'bulletproof',
6 '完全', 'ゼロ', '確実', '絶対', '必ず', '爆速', '100%', '100%',
7]
8
9// Where published writing usually lives. Matched against the path with forward slashes.
10export const DEFAULT_PATHS = [
11 '/content/', '/posts/', '/blog/', '/drafts/', '/newsletter/', '/announcements/', '/press/',
12 '/public/*.html', '/site/*.md', 'README.md', 'CHANGELOG.md',
13]
14
15// A comma separated setting as a clean list; the default when empty.
16export function list(setting, fallback) {
17 const items = String(setting ?? '')
18 .split(',')
19 .map((s) => s.trim())
20 .filter(Boolean)
21 return items.length ? items : fallback
22}
23
24// Whether a file is published writing: a pattern is a path fragment, and `*` matches within one folder level.
25export function isPublished(filePath, patterns) {
26 const p = String(filePath || '').replace(/\\/g, '/')
27 return patterns.some((pat) => {
28 if (!pat.includes('*')) return p.includes(pat)
29 const re = new RegExp(pat.split('*').map((s) => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*') + '$')
30 return re.test(p)
31 })
32}
33
34// Each overclaiming word found, with a little text around it (at most 8 hits).
35export function findOverclaims(text, words) {
36 const src = String(text || '')
37 const lower = src.toLowerCase()
38 const hits = []
39 for (const w of words) {
40 const needle = w.toLowerCase()
41 let i = lower.indexOf(needle)
42 while (i !== -1 && hits.length < 8) {
43 const around = src.slice(Math.max(0, i - 16), i + w.length + 16).replace(/\s+/g, ' ')
44 hits.push('"' + w + '" in: ' + around)
45 i = lower.indexOf(needle, i + needle.length)
46 }
47 }
48 return hits
49}
50
51// The message Claude reads when a write or a typed post is stopped.
52export function denyMessage(where, hits) {
53 return [
54 'overclaim-guard: ' + where + ' promises more than was measured:',
55 ...hits.map((h) => ' - ' + h),
56 'Rewrite it with what was actually measured, or say what is not known. If the word is intended, ask the person, then run /overclaim-allow to let the next one through.',
57 ].join('\n')
58}
59
60// The text a browser tool is about to type or paste, or '' when the call types nothing.
61export function typedText(tool, input) {
62 const t = String(tool || '')
63 if (/__computer$/.test(t)) return input && input.action === 'type' ? String(input.text || '') : ''
64 if (/__javascript_tool$/.test(t)) {
65 const code = String((input && input.text) || '')
66 return /insertText|ClipboardEvent|clipboardData|innerText\s*=|textContent\s*=|\.value\s*=/.test(code) ? code : ''
67 }
68 if (/__form_input$/.test(t)) return input && typeof input.value === 'string' ? input.value : ''
69 return ''
70}
71