Nudges the model back to Traditional Chinese right after a step drifts into English, before it snowballs.

Personal Claude Code plugins and mods.
| Plugin | What it does |
|---|---|
gh-quota | Keeps agents from burning the shared GitHub API quota: gh-wait (waits on CI with ETags), a hook that blocks gh polling loops and anonymous curl, and a skill on how to wait |
gh-quota-meter | Logs the GitHub API quota (REST/GraphQL) to meter.log every 2 min and toasts once per reset window when it drops below 1000; the status-line entry is opt-in (status option), and prompt-glance's HUD shows the quota by default |
prompt-glance | Two-row HUD above the prompt: model, branch, context and plan-window gauges, prompt-cache hit rate and countdown |
zh-steady | Nudges the model back to Traditional Chinese when a step drifts into English |
/plugin marketplace add audichuang/mods
/plugin install gh-quota --marketplace audichuang/mods
/plugin install gh-quota-meter --marketplace audichuang/mods
/plugin install prompt-glance --marketplace audichuang/mods
/plugin install zh-steady --marketplace audichuang/modshooks/register.ts 63 lines1import type { PromptSubmitInput, Register } from 'claude-code'
2
3export const REMINDER =
4 '提醒:你上一段給使用者看的文字是英文。接下來所有給使用者看的文字(包括工具呼叫之間的過場說明)都用繁體中文;程式碼、指令、識別字、錯誤原文保持原樣。'
5
6// Mostly-English test: Latin letters weighed ~4 per word against CJK chars,
7// after dropping code, URLs and paths. Too short to judge -> undefined.
8// URLs and paths match ASCII only, so CJK glued to them stays; a path starts
9// only at a token's start, so a long token without '/' is scanned once.
10export function isEnglish(text: string): boolean | undefined {
11 const t = text
12 .replace(/```[\s\S]*?```/g, '')
13 .replace(/`[^`]*`|https?:\/\/[\x21-\x7e]+|(?<![\w.~-])[\w.~-]*\/[\w.~\/-]*/g, '')
14 const latin = (t.match(/[A-Za-z]/g) ?? []).length
15 const cjk = (t.match(/[一-鿿]/g) ?? []).length
16 if (latin + cjk < 40) return undefined
17 return cjk / (latin / 4 + cjk) <= 0.3
18}
19
20// The person's own words: typed, through Remote Control, the SDK host, a Slack
21// ping, a same-user channel the engine can't classify, or a plugin's submission
22// asUser. Notifications, peers and continuations are not.
23function typedByUser(e: PromptSubmitInput): boolean {
24 if (e.isMeta) return false
25 const o = e.origin
26 return (
27 o.kind === 'composer' || o.kind === 'bridge' || o.kind === 'sdk' ||
28 o.kind === 'slack-ping' || o.kind === 'unclassified' ||
29 (o.kind === 'plugin' && o.asUser === true)
30 )
31}
32
33export const register: Register = on => {
34 // Main loop only; subagent text never reaches the person directly.
35 let drifted = false
36 let userWantsEnglish = false
37
38 on('prompt.submit', ($, e, next) => {
39 if (typedByUser(e)) {
40 const en = isEnglish(e.text)
41 if (en !== undefined) userWantsEnglish = en
42 drifted = false
43 }
44 return next(e)
45 })
46
47 on('turn.step', async function* ($, e, next) {
48 const r = yield* next(e)
49 if (!e.agentId) {
50 const en = isEnglish(r.answer)
51 if (en !== undefined) drifted = en && !userWantsEnglish
52 }
53 return r
54 })
55
56 on('tool.call', async ($, e, next) => {
57 const r = await next(e)
58 if (!drifted || e.agentId || r.deny !== undefined) return r
59 drifted = false
60 return { ...r, context: [...(r.context ?? []), REMINDER] }
61 })
62}
63