Content
Hook
Hooks
What mojiemoji-github runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
> /plugin marketplace add jozobeer/mojiemoji-plugin > /plugin install mojiemoji-github@mojiemoji-plugin
Ships with mojiemoji-github. Installing the plugin gets these hooks.
What fires, and when
PreToolUse
- Matches
Bash|mcp__.*github.*"${CLAUDE_PLUGIN_ROOT}/hooks/mojiemoji_japanese_gate.py"
PostToolUse
- Matches
Edit|Write|MultiEdit"${CLAUDE_PLUGIN_ROOT}/hooks/mojiemoji_md_edit_warn.py"
Where it lives
- hooks/mojiemoji_japanese_gate.pyRunsGitHub
Read the script
#!/usr/bin/env python3 """PreToolUse hook: gate Japanese (and opt-in English/Latin #148) GitHub body submissions without properly-styled mojiemoji stamps. Fires on two posting paths: 1. Bash tool with `gh` posting the body: - `gh (issue|pr|release) (create|comment|review)` (high-level), OR - `gh api .../reviews|comments|issues|releases ...` (raw REST POST, used by skills like cross-repo-review that batch-publish reviews). 2. MCP GitHub tools whose `tool_input` carries a Japanese `body` field, including nested review `comments[].body` fields. The MCP matcher uses both server-alias signals (anything with `github` in the namespace) AND known GitHub-specific tool name patterns (`*pull_request*`, `*issue_write`, `add_issue_comment`, `*release*`, etc.) so installations that aliased the GitHub MCP server to a non-`github` name are still covered. Title / commit_message / file content / description are intentionally NOT inspected — only the `body` posting-prose field is inspected, and each body value must be decorated on its own. And EITHER: 1. inspected text has zero `mojiemoji.jozo.beer` URLs, OR 2. at least one mojiemoji URL is missing any of the required style parameters (`background=transparent`, `font=*`, `color=*`, `animation=*`, `outline=darker`, `outline_width=2`), OR 3. a URL uses a non-canonical font/animation, an invalid outline value, pairs a color-shifting animation with an outline, uses a Tailwind 600+ color (invisible on dark mode), or contains a 3-kanji single-stamp text (must split as 2+1). The Bash path also reads referenced body files (`--body-file PATH` / `--input PATH` / `-F body=@PATH`) and interpreter-invoked scripts so file-routed / dynamically-built bodies are covered too. When triggered, blocks the tool call (exit 2) and prints reminder to stderr so Claude sees it before submission. Bypass: include `MOJIEMOJI_HOOK_DISABLED=1` anywhere in the inspected text — for Bash that's the command line (prefix idiom matches the git pre-commit hook), for MCP that's the body itself. Implementation lives in `hooks/gate/` — this script is the thin entry pipeline that wires the routing / validator modules together. See https://github.com/jozobeer/mojiemoji-plugin/issues/101. """ from __future__ import annotations import json import sys from pathlib import Path # `hooks/gate/__init__.py` already splices the skill scripts dir onto # sys.path, but Claude Code invokes this file as `python3 hooks/...` # rather than as a package member, so `gate` itself isn't importable # until `hooks/` is on the path. Splice both — hooks/ for `import gate`, # scripts/ for any direct `from lib.X import Y` consumers downstream. _HOOKS_DIR = Path(__file__).resolve().parent if str(_HOOKS_DIR) not in sys.path: sys.path.insert(0, str(_HOOKS_DIR)) from gate.extract import JP_RE, LATIN_RE, MOJI_URL_RE, extract_inspect_text # noqa: E402 from gate.extract import forces_pr_body, is_pr_body_submission, pr_body_target_repo # noqa: E402 from lib.plugin_root import plugin_root # noqa: E402 from gate.validators import ( # noqa: E402 PIPELINE, validate_catalog_leftovers, validate_schema_version, ) from lib.repo_policy import POLICY_LEAKS, POLICY_UNKNOWN, repo_policy_state # noqa: E402 _PR_BODY_LEAK_REMINDER = ( "🚫 このリポジトリは PR body を commit message にコピーする設定です\n" "\n" "検出: squash / merge commit message が `PR_BODY` のリポジトリに、mojiemoji\n" "stamp を含む PR body を投稿しようとしています。GitHub が PR body を squash /\n" "merge commit に転記するため、`<img src=\"https://mojiemoji.jozo.beer/...\">`\n" "の HTML が commit 履歴に恒久的に残ってしまいます (issue #138)。\n" "\n" "## 対処\n" "1. PR body から mojiemoji stamp を外して投稿する (推奨。本文の装飾は不要)\n" "2. どうしても装飾したいなら、投稿経路に応じて FORCE を明示する:\n" " - Bash: command 先頭に `MOJIEMOJI_FORCE_PR_BODY=1` を付ける\n" " - MCP: body 内に `MOJIEMOJI_FORCE_PR_BODY=1` を含める\n" "\n" "issue / review / comment など他の surface は通常どおり装飾して構いません。\n" f"詳細: {plugin_root()}/skills/mojiemoji-github/SKILL.md\n" ) def main() -> int: try: data = json.load(sys.stdin) except Exception: return 0 inspect_texts = extract_inspect_text(data) if inspect_texts is None: return 0 jp_texts = [text for text in inspect_texts if JP_RE.search(text)] en_texts = [text for text in inspect_texts if LATIN_RE.search(text)] # English/Latin opt-in support (#148). Pure English bodies are NOT gated # by default (to avoid forcing stamps on English-first users). # Opt in for a submission by including the marker: # MOJIEMOJI_ENGLISH_GATE=1 (in Bash command or inside the body text) english_enabled = any( "MOJIEMOJI_ENGLISH_GATE=1" in (t or "") for t in (inspect_texts or []) ) or "MOJIEMOJI_ENGLISH_GATE=1" in ( (data.get("tool_input", {}) or {}).get("command", "") ) if not jp_texts and not (english_enabled and en_texts): return 0 # Determine which bodies to enforce stamping rules on. Japanese and # opt-in English bodies are both active when a payload mixes them. active = [ text for text in inspect_texts if JP_RE.search(text) or (english_enabled and LATIN_RE.search(text)) ] cwd = data.get("cwd", "") if is_pr_body_submission(data) and not forces_pr_body(data): owner, repo = pr_body_target_repo(data) or (None, None) state = repo_policy_state( owner=owner, repo=repo, cwd=Path(cwd) if cwd else None, ) has_stamp = any(MOJI_URL_RE.search(text) for text in active) if not has_stamp and state in (POLICY_LEAKS, POLICY_UNKNOWN): return 0 if has_stamp and state == POLICY_LEAKS: sys.stderr.write(_PR_BODY_LEAK_REMINDER) return 2 for inspect_text in active: urls = MOJI_URL_RE.findall(inspect_text) for stage in PIPELINE: rc - hooks/mojiemoji_md_edit_warn.pyRunsGitHub
Read the script
#!/usr/bin/env python3 """PostToolUse hook: warn (not block) when an Edit/Write/MultiEdit on a documentation `*.md` file leaves prestamp drift. Local file edits never flow through the gh / MCP gate, so README / docs / SKILL.md / agents prompts can silently drift away from the catalog without anyone noticing — exactly the dogfood-followup problem the project keeps re-discovering (#91). This hook reads the file after the edit lands, runs `prestamp.py` over it in a subprocess, and if the output differs from the current contents, emits a unified diff to stderr with the suggested transform. It never blocks — the goal is awareness, not enforcement. The CI drift check (#91 / catalog-drift-check sibling) is the hard gate. Matched file paths (anything else is silently ignored): - any `*.md` under the repo root, restricted to: - `README.md` - `docs/**/*.md` - `agents/**/*.md` - `skills/**/SKILL.md` - `CHANGELOG.md` Authors who want a clean before/after region can wrap it with `<!-- mojiemoji:off -->` / `<!-- mojiemoji:on -->` — prestamp respects the markers, so this hook will be silent for those segments. Exit code is always 0. Output on stderr only fires when there is something the author should look at. """ import json import os import re import subprocess import sys from pathlib import Path SKILL_MD_SUFFIX = "SKILL.md" JP_RE = re.compile(r"[\u3040-\u309f\u30a0-\u30ff\u4e00-\u9fff]") # Also consider Latin for English/i18n drift warnings (#148) LATIN_RE = re.compile(r"[A-Za-z][A-Za-z'-]{2,}") def _is_documentation_md(path: Path, repo_root: Path) -> bool: try: rel = path.resolve().relative_to(repo_root.resolve()) except (ValueError, OSError): return False rel_str = str(rel).replace(os.sep, "/") if rel_str == "README.md" or rel_str == "CHANGELOG.md": return True if rel_str.startswith("docs/") and rel_str.endswith(".md"): return True if rel_str.startswith("agents/") and rel_str.endswith(".md"): return True if rel_str.startswith("skills/") and rel_str.endswith(SKILL_MD_SUFFIX): return True return False def _has_japanese(text: str) -> bool: return JP_RE.search(text) is not None def _has_latin(text: str) -> bool: return LATIN_RE.search(text) is not None def _find_repo_root(start: Path) -> Path | None: cur = start.resolve() for parent in (cur, *cur.parents): if (parent / ".git").exists() or (parent / ".claude-plugin").exists(): return parent return None def _file_path_from_payload(payload: dict) -> Path | None: tool_input = payload.get("tool_input") or {} raw = tool_input.get("file_path") if isinstance(raw, str) and raw: return Path(raw) return None def _run_prestamp(text: str, plugin_root: Path) -> tuple[str, int]: script = plugin_root / "skills" / "mojiemoji-github" / "scripts" / "prestamp.py" if not script.exists(): return "", -1 proc = subprocess.run( [sys.executable, str(script)], input=text, capture_output=True, text=True, timeout=15, ) return proc.stdout, proc.returncode def _unified_diff(original: str, transformed: str, label: str) -> str: import difflib diff_lines = difflib.unified_diff( original.splitlines(keepends=True), transformed.splitlines(keepends=True), fromfile=f"a/{label}", tofile=f"b/{label}", n=2, ) return "".join(diff_lines) def main() -> int: try: payload = json.loads(sys.stdin.read() or "{}") except json.JSONDecodeError: return 0 tool_name = payload.get("tool_name") or "" if tool_name not in {"Edit", "Write", "MultiEdit"}: return 0 file_path = _file_path_from_payload(payload) if file_path is None: return 0 if file_path.suffix != ".md": return 0 plugin_root_env = os.environ.get("CLAUDE_PLUGIN_ROOT") plugin_root = Path(plugin_root_env) if plugin_root_env else None repo_root = _find_repo_root(file_path.parent) or plugin_root if repo_root is None: return 0 if not _is_documentation_md(file_path, repo_root): return 0 if not file_path.exists(): return 0 try: current = file_path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): return 0 if not (_has_japanese(current) or _has_latin(current)): return 0 prestamp_root = plugin_root or repo_root transformed, rc = _run_prestamp(current, prestamp_root) if rc != 0: return 0 if transformed == current: return 0 try: rel_label = str(file_path.resolve().relative_to(repo_root.resolve())) except (ValueError, OSError): rel_label = file_path.name diff = _unified_diff(current, transformed, rel_label) if not diff.strip(): return 0 sys.stderr.write( "📎 mojiemoji prestamp drift detected on doc edit " f"({rel_label}). The catalog would transform this further " "— run `prestamp.py` on the file if you want to apply, or " "wrap the affected region in `<!-- mojiemoji:off -->` … " "`<!-- mojiemoji:on -->` if it should stay raw. " "Suggested diff:\n\n" ) sys.stderr.write(diff) sys.stderr.write("\n") return 0 if __name__ == "__main__": sys.exit(main())
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withmojiemoji-github
mojiemoji.jozo.beer のスタンプ画像で本語の GitHub Markdown (issue / / レビュー / リプライ / リリースノート) を に — これは【マジで】やばい【バグ】ですね のように ワードだけスタンプ化 する mid-sentence インライン強調を でに埋め込む Claude Code 🚀 的にはこの README 自体が dogfooding なので、上から下までスタンプまみれ
Get the whole plugin
Stats
4
Stars
0
Forks
Active
Maintenance
Python
Language
MIT
License
25d ago
Last commit
4mo ago
Created
Repo: jozobeer/mojiemoji-plugin

