Claude Code plugins to protect your main branch and read responses aloud with Japanese voices
翻译准备中
AI 摘要 · README
插件Python · ShellGitHub @lacolaco自行安装运营者未确认
试用过了吗? 点一下就好,无需登录。
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
安装型项目无需部署域名也可验证。个人仓库可通过 GitHub 登录验证,组织仓库可使用上方的仓库验证文件。
正在加载完整 README。 以下为已保存的摘录。
Claude Code plugins by lacolaco.
Blocks git subcommands that would modify the protected branch (defaults to main) when checked out in Claude Code.查看完整 README ↗Claude Code plugins by lacolaco.
| Plugin | Description |
|---|---|
| protect-main-branch | Prevent git operations that would modify the main branch (configurable) |
| session-tts | Read Claude Code responses aloud locally with a different Japanese voice per session. Instructs Claude to deliver mid-turn progress narration via a synchronous Bash call into the say adapter. Permission prompts include the workspace name. ON by default; controllable via SESSION_TTS_ENABLED env var; playback volume is adjustable via /session-tts:volume. Engine and voices are managed automatically (Apple Silicon) |
Blocks git subcommands that would modify the protected branch (defaults to main) when checked out in Claude Code.
commit, push, merge, rebase, reset, cherry-pick, revert, am. All other operations (Write, Edit, git pull, git status, git switch, etc.) pass through unchanged.PROTECT_MAIN_BRANCH_ALLOWED_SUBCOMMANDS (see Configuration).PROTECT_MAIN_BRANCH_DISABLE=1 (see Configuration). Useful for solo-author repos with linear-history-on-main policies where the protection is the wrong default.git pull is intentionally allowed so the protected branch can be synced with its upstream. To introduce changes to the protected branch, do the work on a feature branch and merge it via a PR.
When blocked, the hook returns:
Cannot run `git <subcommand>` on <branch> branch. Create a feature branch first.
Three environment variables can be set in your Claude Code settings.json (at user scope ~/.claude/settings.json, project scope .claude/settings.json, or local .claude/settings.local.json). Settings are loaded at session start, so a new session must be opened for changes to take effect.
PROTECT_MAIN_BRANCH_DISABLE — turn the hook off entirelyWhen set to "1", the hook exits 0 unconditionally and allows every git operation on every branch, including the protected one. The other two variables are not consulted. Defaults to unset (hook active).
{
"env": {
"PROTECT_MAIN_BRANCH_DISABLE": "1"
}
}
Place this in .claude/settings.local.json of a specific repository to opt that repository out of protection while keeping the hook active everywhere else. Any value other than "1" (including "0", "true", empty string) keeps the hook active — the check is strictly = "1" so the kill switch can only be tripped intentionally.
PROTECT_MAIN_BRANCH_NAME — protected branch namesSpace-separated list of branch names to protect. Defaults to main.
{
"env": {
"PROTECT_MAIN_BRANCH_NAME": "main master develop"
}
}
PROTECT_MAIN_BRANCH_ALLOWED_SUBCOMMANDS — per-user allowlistSpace-separated list of git subcommands that should be exempted from the block even when run on a protected branch. Subcommands not in the default blocklist (e.g. pull, fetch) are unaffected by this setting since they are never blocked.
{
"env": {
"PROTECT_MAIN_BRANCH_ALLOWED_SUBCOMMANDS": "merge revert"
}
}
The allowlist accepts any subcommand, including commit and push — the plugin trusts your configuration. Use this when your workflow legitimately requires running a normally-blocked subcommand on the protected branch (for example, merging a feature branch back into main locally).
/plugin marketplace add lacolaco/claude-plugins
/plugin install protect-main-branch@lacolaco-plugins
jq must be installedReads Claude Code's responses aloud on your local machine. Each Claude Code session is automatically assigned one of three different Japanese voices in a fixed rotation, so when several sessions are running you can tell them apart by voice. Synthesis happens locally; no external API is called during playback.
The plugin subscribes to six hook events:
SessionStart — assigns this session a voice from the 3-slot rotation (the assignment is stored at ~/.claude/session-tts/sessions/$session_id/speaker and stays stable across clear/compact re-fires). When SESSION_TTS_ENABLED=0, the voice is still assigned but the session starts silenced (no context injection, no engine bootstrap, no announcement). It also kicks off a background engine bootstrap that is idempotent: typical re-runs do nothing.SessionEnd — fires when this session terminates (/clear, /compact, logout, etc.). SIGTERMs any in-flight playback for this session via the per-session pidfile (sessions/$session_id/playback) so audio doesn't outlive the session that started it. The session directory and its contents are intentionally left in place so the same session_id keeps its voice across /clear.Stop — fires when Claude finishes a normal response; speaks last_assistant_messageStopFailure — fires when the turn ends due to an API error; speaks last_assistant_messageNotification with the permission_prompt matcher only — a tool needs approval → speaks 「で承認待ちです」, where <workspace> is the basename of cwd from the hook input (e.g. 「claude-pluginsで承認待ちです」). Falls back to 「承認待ちです。」 if cwd is missing. (Other Notification subtypes including idle_prompt are intentionally not subscribed.)PostToolUse:TodoWrite — task transition: nudge to narrate completion → next taskPreToolUse:Monitor — about to watch a long-running background task: nudge to narrate what/whyPreToolUse:Agent — about to dispatch a sub-agent: nudge to narrate the delegated work + planUserPromptSubmit — new turn boundary: nudge to narrate at milestones if this becomes multi-step
All four dispatch through scripts/remind-say.sh <trigger> and return hookSpecificOutput.additionalContext (or, for UserPromptSubmit, stdout) reminding Claude to call say.sh via Bash synchronously (no run_in_background). The model still owns wording and 枕詞. No-op when the session has no voice or has been silenced. High-frequency tools (Write/Edit/Bash) are deliberately not matched to avoid context spam.On the first session ever, the SessionStart hook downloads the local TTS engine binary into ~/.claude/session-tts/engine/ and installs the three voice models. From then on it just probes the engine's health endpoint (sub-100 ms) and exits.
The plugin is structured around a single core, scripts/say-response.py, that takes plain UTF-8 text on stdin and a per-session voice via SESSION_TTS_SPEAKER_ID env. It:
. (e.g. say.sh, src/foo.tsx, 0.7.3, 127.0.0.1) to a space so the engine doesn't treat the period as a sentence boundary and insert a long pause inside a filename, path, or version number. Sentence-ending . (followed by whitespace or end of text) is left alone.! (e.g. GitLab MR !107, !42) before synthesis. The ! there is a reference sigil, not an exclamation, and the engine would otherwise pause between it and the number. Mid- or end-of-word ! (Done!, Wow! Great) keeps its exclamation prosody.―—–─━ (2+ in a row, e.g. Insight ―――, ━━━ section ━━━) to a single space so the engine doesn't read the separators aloud or pause on them. A lone — / – is left alone so it can still function as prose punctuation.。.!?!? and ASCII .) inside each paragraph. Commas (、,,) are deliberately not split points — every chunk adds prePhonemeLength lead-in silence plus an afplay device-open transient, so splitting at commas would turn the engine's natural micro-pause into a much longer unnatural gap. The first chunk is intentionally small (≤ 60 chars) so the first audible word arrives quickly even on long responses; later chunks are larger (≤ 250 chars) for natural cadence. Markdown headings (## title) are not emitted as their own paragraph — the heading text is folded into the next paragraph with a 。 separator, so a single-word heading does not become a 2-character chunk bracketed by audible silence.httpx.Client and pushes each WAV onto a playback queue so audio starts as soon as the first chunk is ready (synthesis and playback run in parallel)Around the core are thin adapters, one per input source. Each adapter is responsible for whatever its own input contract dictates (hook payload schemas, notification fields, skill arguments) and ends with a plain-text call into the core via scripts/lib/voice-context.sh:
| Adapter | Input source | Text / Output |
|---|---|---|
scripts/dispatch.sh | Stop / StopFailure hook payload (stdin JSON) | last_assistant_message (spoken) |
scripts/notify-permission.sh | Notification:permission_prompt payload | <workspace>で承認待ちです。 (spoken) |
skills/say/say.sh | Bash tool argv (called by Claude synchronously for mid-turn narration) | the argument verbatim (spoken) |
scripts/remind-say.sh <trigger> | hook stdin (one of: TodoWrite, Monitor, Agent, UserPromptSubmit) | JSON additionalContext reminder (todo/monitor/agent) or plain stdout (prompt). Not spoken. |
The shared helper scripts/lib/voice-context.sh resolves the per-session speaker (or returns failure if the session has no voice or has been silenced) and forwards text to the core. Hook payload schemas never leak into the core.
The Python runtime is isolated under python/ and managed by uv; adapters call uv run --directory ${CLAUDE_PLUGIN_ROOT}/python.
In addition to the hook-triggered narration of full responses, Claude is instructed to deliver verbal task-progress reports during autonomous, multi-step work so the user can follow progress by ear without reading every message.
The model invokes skills/say/say.sh through the Bash tool, synchronously — run_in_background is intentionally not passed:
Bash(
command: bash "${CLAUDE_PLUGIN_ROOT}/skills/say/say.sh" "<lead-in + body, ≤100 Japanese chars>",
description: "TTS report"
)
say.sh echoes the phrase to stdout before speaking it (also when nothing is spoken because the session is silenced or has no voice), so the Bash result already shows the report on screen. The injected instructions tell the model to report through the say.sh call alone and not repeat the same sentence in chat text.
The call blocks for the duration of synthesis + playback. The 100-character cap and milestone-only discipline keep that block short enough not to disrupt the turn.
skills/say/say.sh is the same implementation used by the Stop / Notification hook adapters; it goes through voice-context.sh::resolve_speaker → speak_text and automatically skips speaking if the session has been silenced via /session-tts:tts off.
Suggested calling moments:
Constraints: under ~100 Japanese characters per call, one phrase per invocation, reported at milestones rather than at each tool call, and always opened with a brief lead-in phrase (枕詞) like 「報告です。」「問題発生です。」「発見です。」「方針転換です。」 so the listener can orient before the body. Not used for the final turn message (Stop already handles that).
The plugin nudges Claude toward making this call via two mechanisms:
SessionStart instruction injected through the hook's stdout (declares the calling moments, lead-in rule, and the exact synchronous Bash call shape).scripts/remind-say.sh <trigger>, that re-surface the narration rule at point-in-time milestones — PostToolUse:TodoWrite (task transition), PreToolUse:Monitor (long watch starting), PreToolUse:Agent (sub-agent dispatch), UserPromptSubmit (new turn boundary). Each injects a short reminder via hookSpecificOutput.additionalContext (or stdout for UserPromptSubmit) and does not produce audio — the model owns wording and 枕詞 because hook payloads are typically English/terse and the engine is Japanese. Each reminder ends with Skip if you just narrated in the immediately preceding step. so the model self-throttles when several triggers fire close together.Actual frequency is still up to model judgment. say.sh itself skips speaking if TTS has been silenced via /session-tts:tts off, so accidental calls during silenced sessions don't produce audio.
/session-tts:ttsVoice is ON by default in every new session. Use the /session-tts:tts skill to override:
/session-tts:tts off # silence THIS session
/session-tts:tts on # re-enable
/session-tts:tts toggle # flip
/session-tts:tts status # show current state (default)
The skill toggles ~/.claude/session-tts/sessions/$CLAUDE_CODE_SESSION_ID/silenced and is independent of the voice assignment, so silencing then re-enabling preserves the same voice. Switching to off additionally kills any utterance still playing for this session (via the per-session playback pidfile), so the silence takes effect immediately rather than draining the remaining chunk queue. Other concurrent sessions are unaffected. The same flag is honored by say.sh (used by both the mid-turn narration call described above and the Stop / Notification hook adapters), so silenced sessions stay silent across every entry point.
When tts on is called on a session that was silenced at start (via SESSION_TTS_ENABLED=0), the skill performs late activation: removes the silenced flag, ensures the engine is running, outputs the narration context, and announces "TTSを開始します。" — enabling full TTS mid-session without requiring a restart.
/session-tts:volumeEvery chunk is played through afplay --volume <coefficient>. The default coefficient is 0.8, which keeps TTS quieter than other audio (notifications, music) when system volume is up — macOS has no native way to make afplay follow the system "alert volume", so this is the simplest substitute. Use the /session-tts:volume skill to override:
/session-tts:volume 0.5 # set the coefficient to 0.5
/session-tts:volume status # show the current value (default)
/session-tts:volume reset # restore the built-in default (0.8)
The chosen value is persisted to ~/.claude/session-tts/volume and read by say-response.py for every chunk it plays. The setting is per-user, not per-session — adjusting it affects every active and future session, including ones already running (the next chunk picks up the new value). Values outside [0.0, 1.0] are rejected and the previous setting (or the built-in default) stays in effect.
All configuration is done via environment variables in Claude Code's settings.json (env field). Project-level settings override global, so you can set a global default and selectively override per-project. Settings take effect at session start.
// ~/.claude/settings.json (global default)
{
"env": {
"SESSION_TTS_ENABLED": "1",
"SESSION_TTS_VOLUME": "0.8"
}
}
// <project>/.claude/settings.json (per-project override)
{
"env": {
"SESSION_TTS_ENABLED": "0"
}
}
| Variable | Values | Default | Description |
|---|---|---|---|
SESSION_TTS_ENABLED | 1/true/yes or 0/false/no | 1 | Controls whether TTS activates on session start. When disabled, the voice is still assigned but the session starts silenced. /session-tts:tts on performs late activation. |
SESSION_TTS_VOLUME | Decimal in [0.0, 1.0] | 0.8 | Default afplay --volume coefficient. Used when the volume file does not exist. /session-tts:volume writes to the file and takes priority. |
SESSION_TTS_ENGINE_URL | URL | http://127.0.0.1:10101 | TTS engine HTTP endpoint. For advanced use only (custom engine port or remote engine). |
Each configurable aspect has a clear override hierarchy (highest priority first):
/session-tts:tts off|on > SESSION_TTS_ENABLED env var > default ON/session-tts:volume) > SESSION_TTS_VOLUME env var > hardcoded 0.8SESSION_TTS_ENGINE_URL env var > hardcoded http://127.0.0.1:10101/plugin marketplace add lacolaco/claude-plugins
/plugin install session-tts@lacolaco-plugins
After installing, every new session speaks by default with a rotating voice. Use /session-tts:tts off to silence a particular session, or set SESSION_TTS_ENABLED=0 to start silenced by default.
uv on PATHjq on PATHThe bundled voices are licensed under ACML 1.0 and downloaded from AivisHub on first use. ACML 1.0 permits personal use with credit; check the per-voice terms on AivisHub before any other use (commercial use, redistribution, derivative works, etc).
MIT
afplay