Skip to content
Development
Hook

Hooks

What encoding-guard runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
encoding-guard
161 command5 hooks
Install
> /plugin marketplace add ymonster/claude_encoding_guard
> /plugin install encoding-guard@claude-encoding-guard

Ships with encoding-guard. Installing the plugin gets these hooks.

What fires, and when

PreToolUse

  • MatchesRead|Edit|Writeuv run --script "${CLAUDE_PLUGIN_ROOT}/hooks/encoding_guard.py" pre

PostToolUse

  • MatchesEdit|Writeuv run --script "${CLAUDE_PLUGIN_ROOT}/hooks/encoding_guard.py" post

Stop

  • uv run --script "${CLAUDE_PLUGIN_ROOT}/hooks/encoding_guard.py" restore-all

SessionStart

Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.

  • Matchesresumeuv run --script "${CLAUDE_PLUGIN_ROOT}/hooks/encoding_guard.py" restore-all

SessionEnd

  • uv run --script "${CLAUDE_PLUGIN_ROOT}/hooks/encoding_guard.py" restore-all
Read hooks/hooks.json

In the plugin's words

How encoding-guard describes its own hook set.

Encoding guard hooks - preserve non-UTF-8 file encodings when Claude Code edits

Where it lives

  • hooks/encoding_guard.pyRunsGitHub
    Read the script
    #!/usr/bin/env -S uv run --script
    # /// script
    # requires-python = ">=3.10"
    # dependencies = ["chardet>=5,<6", "binaryornot"]
    # ///
    """
    encoding_guard.py - Preserve file encoding and line endings when Claude Code edits files.
    
    Strategy:
        1. PreToolUse (Read):  detect encoding + line endings, convert to UTF-8 so Claude reads correctly
        2. PreToolUse (Edit):  file already UTF-8 (cache exists), skip
        3. PostToolUse (Edit): restore original encoding + line endings from session cache
        4. Stop / SessionEnd / SessionStart(resume) (restore-all):
           restore any remaining files — turn cleanup, exit backstop, and
           crash-residue healing before the resumed session acts
    
    Recovery commands (user-invoked CLI, not hooks):
        scan / scan-current           read-only report of cache records
        restore-current               restore orphaned files under the cwd
        prune-current                 delete obsolete records under the cwd
    
    Cache layout:
        <tempdir>/.cc_encoding_cache/<session_id>/<sha256(normalized_path)[:16]>.json
    
    Each session gets its own cache directory. Hooks operate strictly on their
    own session's records; only the user-invoked recovery commands may act on
    other sessions' records. Stale EMPTY session dirs (>24h) are removed on
    startup — record-bearing dirs are never auto-deleted.
    Stdin is read as binary (sys.stdin.buffer) to avoid Windows codepage issues.
    """
    
    # Read stdin immediately before any heavy imports (Windows stdin reliability).
    # Hook modes only: CC feeds hooks a pipe that closes. In the recovery CLI
    # modes stdin may be an interactive terminal, where a blind read() would
    # block forever.
    import sys
    _raw_stdin = b""
    if len(sys.argv) > 1 and sys.argv[1] in ("pre", "post", "restore-all"):
        _raw_stdin = sys.stdin.buffer.read()
    
    import contextlib
    import json
    import os
    import hashlib
    import tempfile
    import time
    
    CACHE_ROOT = os.path.join(tempfile.gettempdir(), ".cc_encoding_cache")
    STALE_HOURS = 24
    LOG_FILE = os.path.join(CACHE_ROOT, "encoding_guard.log")
    LOG_MAX_BYTES = 512 * 1024
    
    RESTORE_ENCODINGS = {
        "gb2312", "gbk", "gb18030",
        "big5", "big5hkscs",
        "euc-tw",
        "shift_jis", "euc-jp", "iso-2022-jp",
        "euc-kr",
        "windows-1252", "iso-8859-1",
        "windows-1251",
    }
    
    ENCODING_ALIASES = {
        "gb2312": "gbk",
        "iso-8859-1": "windows-1252",
    }
    
    # Encodings whose chardet detection (>=0.5 confidence + name-in-RESTORE) is
    # reliable enough to skip the binaryornot pre-check. binaryornot's decision
    # tree false-positives mixed-content files in two patterns:
    #   - short CJK files (<~500B Shift_JIS/EUC-JP/EUC-KR/Big5/GB18030)
    #   - mixed Cyrillic + ASCII files (e.g., source code with CP1251 comments)
    # For these encodings chardet's structural validators are strict enough on
    # their own; real binaries cannot reach the confidence threshold.
    STRUCTURAL_TRUSTED = {
        "gbk", "gb18030",
        "big5", "big5hkscs",
        "shiftjis", "eucjp", "euckr", "iso2022jp",
        "windows1251",
    }
    
    
    def _strip_enc(name: str) -> str:
        return name.lower().replace("-", "").replace("_", "")
    
    
    def _log(msg: str):
        sys.stderr.write(f"encoding_guard: {msg}\n")
    
    
    def _flog(event: str):
        """Append one line to the forensic log (CACHE_ROOT/encoding_guard.log).
    
        Hook stderr is captured into transcripts, but operations that run in
        process-shutdown windows (e.g. SessionEnd restore-all after the
        terminal is closed) never reach a transcript — this file is the
        durable trail for every file mutation. Best-effort: never raises.
        """
        try:
            os.makedirs(CACHE_ROOT, exist_ok=True)
            try:
                if os.path.getsize(LOG_FILE) > LOG_MAX_BYTES:
                    os.replace(LOG_FILE, LOG_FILE + ".1")
            except OSError:
                pass
            stamp = time.strftime("%Y-%m-%dT%H:%M:%S")
            with open(LOG_FILE, "a", encoding="utf-8") as f:
                f.write(f"{stamp} {event}\n")
        except OSError:
            pass
    
    
    def normalize_encoding(enc: str) -> str:
        """Map chardet output to a Python codec name.
    
        Apply ENCODING_ALIASES safety mappings (gb2312 -> gbk, iso-8859-1 ->
        windows-1252) when applicable; otherwise preserve chardet's original
        name verbatim. Returning the stripped form (e.g. "windows1251" without
        its dash) breaks codec lookup since Python's codec registry recognizes
        "windows-1251" / "cp1251" but NOT "windows1251".
        """
        stripped = _strip_enc(enc)
        for orig, alias in ENCODING_ALIASES.items():
            if stripped == _strip_enc(orig):
                return alias
        return enc
    
    
    def normalize_path(path: str) -> str:
        """Normalize path for consistent cache keys across pre/post and platforms."""
        return os.path.normcase(os.path.normpath(path))
    
    
    def file_hash(path: str) -> str:
        return hashlib.sha256(normalize_path(path).encode()).hexdigest()[:16]
    
    
    def sanitize_session_id(session_id: str) -> str:
        """Prevent path traversal from untrusted session_id."""
        return session_id.replace(os.sep, "_").replace("/", "_").replace("\\", "_")
    
    
    def session_dir(session_id: str) -> str:
        return os.path.join(CACHE_ROOT, sanitize_session_id(session_id))
    
    
    def cache_path(session_id: str, path: str) -> str:
        return os.path.join(session_dir(session_id), file_hash(path) + ".json")
    
    
    def cleanup_stale_sessions(current_session_id: str):
        """Remove EMPTY stale session dirs only. Never touches another session's records.
    
        An orphaned record is the only copy of "this file on disk should be
        re-encoded back" — deleting it is irreversible information loss, and
        restoring it automatically can override a user who has since accepted
        the UTF-8 state. Cross-session records are therefore acted on only by
        the user, via the scan/restore-current/prune-current commands.
        """
        if not os.path.exists(CACHE_ROOT):
            return
        current_dir_name = sanitize_session_id(current_session_id)
        now = time.time()
        try:
            for name in os.listdir(CACHE_ROOT):
                if name == current_dir_name:
         

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 withencoding-guard

Preserve non-UTF-8 file encodings and line endings when Claude Code edits your files.

Get the whole plugin
Stats
16
Stars
5
Forks
Maintained
Maintenance
Python
Language
MIT
License
1mo ago
Last commit
5mo ago
Created

Repo: ymonster/claude_encoding_guard