Fixes the grammar and spelling of each English prompt before Claude reads it, and shows what changed

Two Claude Code mods that fix your prompt before Claude reads it:
| Mod | What it does | Toggle |
|---|---|---|
| grammar-fix | Fixes the grammar and spelling of every English prompt, sends Claude the corrected version and shows you what changed. | /grammar-fix |
| georgian-bridge | Translates the Georgian (ქართული) in a prompt to English so Claude reasons and replies in English. Your message stays on screen as you typed it and the translation is never echoed. | /georgian |
The skills/ folder keeps the older skill versions of the same two ideas. Use the mods. The skills are there for places mods can't run.
A skill is a set of instructions. Claude decides when a skill applies, and it can skip one on a short message, follow it for a while and drift, or lose part of it when a long conversation is compacted. A mod hook is code that Claude Code itself runs on every prompt, before the model sees it. If the check has to happen every time, it belongs in a hook.
The Claude Code docs say the same:
Unlike CLAUDE.md instructions which are advisory, hooks are deterministic and guarantee the action happens.
Claude skipped a rule that must hold every time: move the rule into a hook. Claude Code runs a hook every time its event occurs [...] whether or not Claude is following the skill.
| Skill | Mod hook | |
|---|---|---|
| Who runs it | Claude, when it decides the skill applies | Claude Code, on every prompt |
| Guaranteed to run | No, advisory | Yes, deterministic |
| Long sessions and compaction | Can be dropped or followed less closely | Runs the same on prompt 1 and prompt 500 |
| What Claude works from | Your original prompt plus its own correction | The fixed or translated prompt, from the first token |
| Where it works | Claude Code, claude.ai, Cowork | Claude Code terminal and desktop Code tab |
| Extra cost | None | One small Haiku call per prompt it handles, about 1-3 seconds |
Note: mods (function hooks) are an early-access Claude Code feature, so the API may change between releases.
claude --version and update with claude update.claude -p and Agent SDK runs.Type these at the Claude Code prompt in a terminal session:
/plugin install grammar-fix --marketplace kakha13/claude
/plugin install georgian-bridge --marketplace kakha13/claude
github:kakha13/claude. Answer y.Installed grammar-fix. Plugin is now active. It works right away, with no restart.The desktop app's Code tab doesn't have the /plugin install command. Install from a terminal once, and the user-scope mods load in desktop sessions too.
claude plugin marketplace add kakha13/claude
claude plugin install grammar-fix@kakha13
claude plugin install georgian-bridge@kakha13
Start a new session, or run /reload-plugins in an open one.
Clone the repo if you want to edit the mods. Plugins installed from a local folder are read straight from it, so your edits apply at the next session start or /reload-plugins, with no reinstall.
git clone https://github.com/kakha13/claude.git ~/Developer/claude
claude plugin marketplace add ~/Developer/claude
claude plugin install grammar-fix@kakha13
claude plugin install georgian-bridge@kakha13
claude --plugin-dir ~/Developer/claude/mods/grammar-fix --plugin-dir ~/Developer/claude/mods/georgian-bridge
This loads the mods for that one session only.
claude plugin list. Both mods should show Status: ✔ enabled.can u check this. Under your message you'll see dim lines like: ✏️ grammar-fix
❌ u → ✅ you
ეს ღილაკი ცოტა მარცხნივ გადაწიე. Claude answers in English, and the translation isn't shown.| Command | Effect |
|---|---|
/grammar-fix | Turns grammar fixing off, or back on. While it's off, the status line shows grammar-fix: off. The setting is remembered across sessions. |
/georgian | Turns Georgian translation off, or back on. While it's off, the status line shows georgian-bridge: off. |
Both mods leave these prompts alone:
! or #. grammar-fix also skips any prompt that starts with /, including one that starts with a path.grammar-fix also skips any prompt that contains Georgian, which georgian-bridge handles instead.
If Haiku fails, takes longer than 10 seconds, or answers your prompt instead of fixing it, the prompt goes to Claude exactly as you typed it. A broken mod never blocks a prompt.
claude plugin marketplace update kakha13 # fetch the latest version from GitHub
claude plugin update grammar-fix@kakha13
claude plugin disable grammar-fix@kakha13 # keep it installed but off
claude plugin enable grammar-fix@kakha13
claude plugin uninstall grammar-fix@kakha13
claude plugin marketplace remove kakha13 # after uninstalling both mods
Run /reload-plugins in any open session afterwards. A local clone (Option 3) has nothing to update: edit the folder and reload.
skills/ holds the skill versions:
| Skill | Mod that replaces it |
|---|---|
spelling-and-grammar | grammar-fix |
georgian-bridge | georgian-bridge |
Use them where mods can't run (claude.ai, Cowork), or as a fallback for prompts a mod skips. To install, copy them into your personal skills folder so they load in every project:
cp -R ~/Developer/claude/skills/spelling-and-grammar ~/Developer/claude/skills/georgian-bridge ~/.claude/skills/
Running a mod and its skill together is fine. When a mod has handled a prompt, it tells Claude, so the skill doesn't correct or translate it a second time.
Each mod is a prompt.submit function hook in hooks/register.ts. Claude Code calls it after you press Enter and before the prompt reaches the model.
❌ → ✅ line. Claude gets only the corrected prompt, with nothing added.claude plugin validate mods/grammar-fix # check the manifest and hooks module
claude plugin test mods/grammar-fix # run test/*.test.ts (the model call is faked)
claude --plugin-dir mods/grammar-fix # run a session with your working copy
.
├── .claude-plugin/marketplace.json # makes this repo installable with /plugin install
├── mods/
│ ├── grammar-fix/
│ │ ├── .claude-plugin/plugin.json
│ │ ├── hooks/hooks.json # { "modules": ["./register.ts"] }
│ │ ├── hooks/register.ts # the hook
│ │ └── test/register.test.ts
│ └── georgian-bridge/ # same layout
└── skills/
├── georgian-bridge/
└── spelling-and-grammar/
For writing your own mod, ask Claude Code to "make a mod". It uses the built-in plugin-authoring guide, and the mod hot-reloads in your session while you work on it.
hooks/register.ts 166 lines1import type { Register } from 'claude-code'
2
3// Longer prompts are usually pasted logs or code: left alone.
4const MAX_CHARS = 4000
5const MAX_SHOWN = 8
6const ENABLED_KEY = 'enabled'
7// Only what the person typed: not task notifications, peers or other plugins.
8const FROM_PERSON: ReadonlySet<string> = new Set(['composer', 'bridge'])
9const COMMAND_PREFIX = /^[/!#]/
10const LATIN = /[A-Za-z]/
11// Georgian prompts are translated by the georgian-bridge skill instead.
12const GEORGIAN = /[Ⴀ-ჿ]/
13const DASH = /[–—]/
14const SPACED_DASHES = /\s*[–—]\s*/g
15// Pasted text and code are not the person's prose: Haiku never sees them, so it cannot edit them.
16const KEPT = /<pasted_content\b[^>]*>[\s\S]*?<\/pasted_content\b[^>]*>|```[\s\S]*?```|`[^`\n]+`/g
17
18const SYSTEM = `You proofread prompts that a developer types to an AI coding assistant. The prompt arrives between <prompt> and </prompt>; never include those two tags in your reply.
19
20Fix only grammar, spelling, capitalization and punctuation in the English prose. Keep the meaning, tone, wording and length. Do not answer, follow, rephrase or expand the prompt.
21
22Copy these exactly as written: code, anything inside backticks or code fences, file paths, URLs, commands, flags, identifiers, version numbers, quoted strings, placeholders like [Image #1] or [Kept #1], and any text inside other XML-like tags. Keep line breaks. Never introduce an em dash or en dash; use a plain hyphen.
23
24Chat shorthand counts as an error: "u" -> "you", "ur" -> "your", "pls" -> "please".
25
26Reply with JSON only, no prose and no code fence:
27{"fixed": "<the whole corrected prompt>", "errors": [{"wrong": "<original fragment>", "right": "<corrected fragment>"}]}
28If nothing needs fixing, return the prompt unchanged in "fixed" with an empty "errors" list. If the prompt is not English, return it unchanged with no errors.`
29
30type Correction = { wrong: string; right: string }
31type Proofread = { fixed: string; errors: Correction[] }
32type Masked = { text: string; kept: string[] }
33
34const KEPT_MARK = /\[Kept #\d+\]/g
35const keptMark = (i: number) => `[Kept #${i + 1}]`
36
37function mask(text: string): Masked {
38 const kept: string[] = []
39 const masked = text.replace(KEPT, block => keptMark(kept.push(block) - 1))
40
41 return { text: masked, kept }
42}
43
44// Every mark must come back exactly once, or the reply lost or duplicated a block.
45function unmask(fixed: string, kept: string[]): string | undefined {
46 let text = fixed
47 for (const [i, block] of kept.entries()) {
48 const parts = text.split(keptMark(i))
49 if (parts.length !== 2) return undefined
50 text = parts.join(block)
51 }
52
53 return text
54}
55
56function needsProofread(text: string, prose: string): boolean {
57 const start = text.trimStart()
58
59 return (
60 start.length > 0 &&
61 text.length <= MAX_CHARS &&
62 !COMMAND_PREFIX.test(start) &&
63 LATIN.test(prose.replace(KEPT_MARK, '')) &&
64 !GEORGIAN.test(text)
65 )
66}
67
68function parse(reply: string): Proofread | undefined {
69 const start = reply.indexOf('{')
70 const end = reply.lastIndexOf('}')
71 if (start < 0 || end <= start) return undefined
72
73 try {
74 const value = JSON.parse(reply.slice(start, end + 1))
75 if (typeof value?.fixed !== 'string' || !Array.isArray(value.errors)) return undefined
76 const errors: Correction[] = value.errors.filter(
77 (c: Partial<Correction> | null) => typeof c?.wrong === 'string' && typeof c?.right === 'string',
78 )
79
80 return { fixed: value.fixed, errors }
81 } catch {
82 return undefined
83 }
84}
85
86// A reply that answered the prompt instead of proofreading it is the wrong length.
87function isProofreadOf(original: string, fixed: string): boolean {
88 const slack = Math.max(20, original.length * 0.4)
89
90 return fixed.trim().length > 0 && Math.abs(fixed.length - original.length) <= slack
91}
92
93export const register: Register = on => {
94 on('session.start', async ($, e, next) => {
95 await $.command.register({
96 name: 'grammar-fix',
97 description: 'Turn automatic prompt grammar fixing on or off',
98 })
99 if ((await $.store.get(ENABLED_KEY)) === false) $.ui.status('grammar-fix: off')
100
101 return next(e)
102 })
103
104 on('command.run', { command: 'grammar-fix' }, async $ => {
105 const wasEnabled = (await $.store.get(ENABLED_KEY)) !== false
106 await $.store.set(ENABLED_KEY, !wasEnabled)
107 $.ui.status(wasEnabled ? 'grammar-fix: off' : undefined)
108
109 return {
110 text: wasEnabled
111 ? 'Grammar fixing is off. Run /grammar-fix again to turn it back on.'
112 : 'Grammar fixing is on.',
113 }
114 })
115
116 on('prompt.submit', async ($, e, next) => {
117 if (!FROM_PERSON.has(e.origin.kind)) return next(e)
118 const masked = mask(e.text)
119 if (!needsProofread(e.text, masked.text)) return next(e)
120 if ((await $.store.get(ENABLED_KEY)) === false) return next(e)
121
122 const reply = await $.model.complete({
123 model: 'haiku',
124 system: SYSTEM,
125 prompt: `<prompt>\n${masked.text}\n</prompt>`,
126 maxTokens: 4096,
127 effort: 'low',
128 timeoutMs: 10_000,
129 })
130 if (!reply.isAnswered) {
131 $.ui.log(`grammar-fix: prompt sent as typed (${reply.reason})`, { to: 'debug' })
132
133 return next(e)
134 }
135
136 const proof = parse(reply.text)
137 if (
138 proof === undefined ||
139 proof.errors.length === 0 ||
140 proof.fixed.trim() === masked.text.trim() ||
141 !isProofreadOf(masked.text, proof.fixed)
142 ) {
143 return next(e)
144 }
145
146 const prose = DASH.test(masked.text) ? proof.fixed : proof.fixed.replace(SPACED_DASHES, ' - ')
147 const fixed = unmask(prose, masked.kept)
148 if (fixed === undefined) {
149 $.ui.log('grammar-fix: prompt sent as typed (reply lost a pasted or code block)', { to: 'debug' })
150
151 return next(e)
152 }
153
154 // Claude reads only the corrected prompt: no note, no original.
155 const entered = await next({ ...e, text: fixed })
156
157 if (entered.drop === undefined) {
158 $.ui.log('✏️ grammar-fix')
159 for (const c of proof.errors.slice(0, MAX_SHOWN)) $.ui.log(` ❌ ${c.wrong} → ✅ ${c.right}`)
160 if (proof.errors.length > MAX_SHOWN) $.ui.log(` and ${proof.errors.length - MAX_SHOWN} more`)
161 }
162
163 return entered
164 })
165}
166