Skip to content
This repository was archived by the owner on Jul 31, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .claude/hooks/.canonical-sha256
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
cfd43f72b3f64bde6cb779703eb13ea6dd2c55ea5ae3dace654bfa95e17345c9 security_guard.py
37ee358245e8be80b00517c32d586449cb669d6d6e02526cc37c0e6728c452d5 format_on_save.py
f06a2180e64db35f96bdb896fbbfa9bf0ebc5090744817f5b87a7f0fbbb7ec61 compact_warning.py
c4437034774443b904e6c7cb529028ea521cf00e7a3f4859a2f964aadf08ffbc spec_orient.py
306d4d68e8e28d09f2ce102c8df6e234c2e4175b1b0c7ccf3dee6f26444584fe _state.py
b62c7991281cf1cc0c34c38f5338fa85eb9c08e7163859d7771e57d0e8e2359c security_guard.py
0983dd23febbae7d2b429d1aa7b21df387811427a664b044a3b19d8821c05dd8 format_on_save.py
0def63915a6bfb0b023a02e504281535ab19d7e2e1e0604901cdb68e345f3990 compact_warning.py
7f924f99fb331b9f30a77f253a69ee4820e2a4f4f257062d58b22530d56ba41a spec_orient.py
d52f97f551a5c8068df2c6afb6faa6b18be795189a9c604d99bd99b815f19315 _state.py
63293f305ff32aab46d1da8b9d28c71ce39b658d2a8572c64024614abdf7dffe _resume_prompt.py
baa145fb6fac25ae7d03a5b655b04aba25bfb77793dcdcaf44acc151394f030b _transcript_size.py
48674de791f509c539417b29214d9c87a33b7934b985597af79711ddd90ea17a _sdk_gate.py
7145a707f6e14473f71e738e3c50df059bf1ad02b3b3b9fa13e5e8b4bc247e72 spec_audit.py
68743464283f0d19f7aca1a491cb122daed037462dd69d7bc3bb1dfe56ae84bb spec_audit.py
76bedca6b34a9126b4bd8d864afad52c6ec72229b26b51f00ff8b4ab673474eb _bootstrap.py
81 changes: 81 additions & 0 deletions .claude/hooks/_bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Shared cross-platform bootstrap helpers for hook scripts.

Stdlib-only. Hook scripts import this (same-directory import, like
``_sdk_gate``) before doing any I/O:

- :func:`ensure_utf8_stdio` — force UTF-8 (``errors="replace"``) on
stdout/stderr. On Windows the console default is often cp1252,
which cannot encode em-dashes/emoji and would crash the hook.
- :func:`read_stdin_utf8` — read the hook payload as UTF-8 bytes
regardless of locale (``sys.stdin.buffer`` bypasses the
locale-encoded text wrapper, which is cp1252 on most Windows
machines).
- :func:`ensure_repo_src_on_path` — make the repo's ``src/``
importable so hooks can lazily import ``attune.*`` without the
POSIX-only ``PYTHONPATH=src python …`` env-prefix syntax in the
hook registration.

Copyright 2026 Smart-AI-Memory
Licensed under the Apache License, Version 2.0
"""

from __future__ import annotations

import sys
from pathlib import Path


def ensure_utf8_stdio() -> None:
"""Reconfigure stdout/stderr to UTF-8 with replacement.

No-op when the streams are already UTF-8 (macOS/Linux default)
or do not support ``reconfigure`` (e.g. pytest capture objects).
"""
for stream in (sys.stdout, sys.stderr):
encoding = getattr(stream, "encoding", None)
if (
encoding
and encoding.lower() not in ("utf-8", "utf8")
and hasattr(stream, "reconfigure")
):
stream.reconfigure(encoding="utf-8", errors="replace")


def read_stdin_utf8(limit: int | None = None) -> str:
"""Read stdin as UTF-8 text, independent of the locale encoding.

Args:
limit: Optional byte cap (e.g. 10_000 to bound hook input).

Returns:
Decoded payload; undecodable bytes become U+FFFD replacements
rather than raising, so a hook never crashes on odd input.
"""
buffer = getattr(sys.stdin, "buffer", None)
if buffer is None: # already detached/wrapped (tests)
return sys.stdin.read() if limit is None else sys.stdin.read(limit)
data = buffer.read() if limit is None else buffer.read(limit)
return data.decode("utf-8", errors="replace")


def ensure_repo_src_on_path() -> None:
"""Insert the repo's ``src/`` directory at the front of ``sys.path``.

Resolved relative to this file — ``parents[3]`` climbs
``scripts/`` → ``hooks/`` → ``attune/`` → ``src/`` — so it works
from any cwd, any worktree, and any platform without env-prefix
syntax in the registration.
"""
try:
src = Path(__file__).resolve().parents[3]
except IndexError:
return
# Require the real package (__init__.py), not just a directory named
# "attune" — from the plugin copy, parents[3] lands OUTSIDE the repo,
# where an unrelated "attune" dir (e.g. a workspace umbrella checkout)
# would otherwise shadow the installed package as a namespace package.
if not (src / "attune" / "__init__.py").is_file(): # plugin copy / moved layout — no-op
return
src_str = str(src)
if src_str not in sys.path:
sys.path.insert(0, src_str)
2 changes: 2 additions & 0 deletions .claude/hooks/_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,8 @@ def _run_git(cwd: Path, *args: str) -> str:
cwd=str(cwd),
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=2.0,
check=False,
)
Expand Down
2 changes: 1 addition & 1 deletion .claude/hooks/compact_warning.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python3
#!/usr/bin/env python
"""Stop-hook compact-warning — fires once per session at threshold.

Stop-hook payloads from Claude Code do NOT expose a context-
Expand Down
12 changes: 11 additions & 1 deletion .claude/hooks/format_on_save.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ def _run_formatter(cmd: list[str], path: str) -> None:
def main() -> None:
"""Read tool result from stdin, format Python files."""
try:
raw = sys.stdin.read()
_buf = getattr(sys.stdin, "buffer", None) # None when tests patch stdin
raw = _buf.read().decode("utf-8", errors="replace") if _buf else sys.stdin.read()
if not raw.strip():
return

Expand Down Expand Up @@ -102,6 +103,15 @@ def main() -> None:


if __name__ == "__main__":
try:
from _bootstrap import ensure_utf8_stdio
except ImportError:
# Vendored copies (sibling .claude/hooks/) may not ship
# _bootstrap.py — degrade to the pre-bootstrap behavior
# rather than crashing the hook.
pass
else:
ensure_utf8_stdio()
from _sdk_gate import exit_if_sdk_subprocess

exit_if_sdk_subprocess()
Expand Down
Loading
Loading