SLOPSHOPPER

zh-steady

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

newguardprompt
v0.2.1no licenseupdated 2026-10-25audichuang/mods/zh-steady
A shopper browsing a rack in a slop shop
README

audi-mods

Personal Claude Code plugins and mods.

PluginWhat it does
gh-quotaKeeps 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-meterLogs 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-glanceTwo-row HUD above the prompt: model, branch, context and plan-window gauges, prompt-cache hit rate and countdown
zh-steadyNudges the model back to Traditional Chinese when a step drifts into English

Install

/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/mods
Source 1 files
hooks/register.ts 63 lines
1import 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