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