Skip to content

Commit 56e54e2

Browse files
chore(hooks): vendor _bootstrap.py + re-sync canonical hooks (attune-ai #1313) (#98)
attune-ai PR #1313 (cross-platform hook layer) added _bootstrap.py to the canonical plugin/hooks/ — stdlib-only UTF-8 stdio hardening + repo-src sys.path bootstrap — and format_on_save.py now imports it (with a graceful ImportError fallback, so vendored copies without it silently skip the UTF-8 hardening). - Add _bootstrap.py to HOOK_FILES in the Makefile sync-hooks section - make sync-hooks ATTUNE_AI_ROOT=~/attune-ai against post-#1313 main (56f84f25e): vendors _bootstrap.py and re-syncs the full hook closure (_state, compact_warning, format_on_save, security_guard, spec_audit, spec_orient) + refreshed .canonical-sha256 Mirrors the spec-status-integrity task-7 sync PRs (rag #192, gui #86, help #23, author #97). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e0502e1 commit 56e54e2

9 files changed

Lines changed: 316 additions & 30 deletions

File tree

.claude/hooks/.canonical-sha256

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
cfd43f72b3f64bde6cb779703eb13ea6dd2c55ea5ae3dace654bfa95e17345c9 security_guard.py
2-
37ee358245e8be80b00517c32d586449cb669d6d6e02526cc37c0e6728c452d5 format_on_save.py
3-
f06a2180e64db35f96bdb896fbbfa9bf0ebc5090744817f5b87a7f0fbbb7ec61 compact_warning.py
4-
c4437034774443b904e6c7cb529028ea521cf00e7a3f4859a2f964aadf08ffbc spec_orient.py
5-
306d4d68e8e28d09f2ce102c8df6e234c2e4175b1b0c7ccf3dee6f26444584fe _state.py
1+
b62c7991281cf1cc0c34c38f5338fa85eb9c08e7163859d7771e57d0e8e2359c security_guard.py
2+
0983dd23febbae7d2b429d1aa7b21df387811427a664b044a3b19d8821c05dd8 format_on_save.py
3+
0def63915a6bfb0b023a02e504281535ab19d7e2e1e0604901cdb68e345f3990 compact_warning.py
4+
7f924f99fb331b9f30a77f253a69ee4820e2a4f4f257062d58b22530d56ba41a spec_orient.py
5+
d52f97f551a5c8068df2c6afb6faa6b18be795189a9c604d99bd99b815f19315 _state.py
66
63293f305ff32aab46d1da8b9d28c71ce39b658d2a8572c64024614abdf7dffe _resume_prompt.py
77
baa145fb6fac25ae7d03a5b655b04aba25bfb77793dcdcaf44acc151394f030b _transcript_size.py
88
48674de791f509c539417b29214d9c87a33b7934b985597af79711ddd90ea17a _sdk_gate.py
9-
7145a707f6e14473f71e738e3c50df059bf1ad02b3b3b9fa13e5e8b4bc247e72 spec_audit.py
9+
68743464283f0d19f7aca1a491cb122daed037462dd69d7bc3bb1dfe56ae84bb spec_audit.py
10+
76bedca6b34a9126b4bd8d864afad52c6ec72229b26b51f00ff8b4ab673474eb _bootstrap.py

.claude/hooks/_bootstrap.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Shared cross-platform bootstrap helpers for hook scripts.
2+
3+
Stdlib-only. Hook scripts import this (same-directory import, like
4+
``_sdk_gate``) before doing any I/O:
5+
6+
- :func:`ensure_utf8_stdio` — force UTF-8 (``errors="replace"``) on
7+
stdout/stderr. On Windows the console default is often cp1252,
8+
which cannot encode em-dashes/emoji and would crash the hook.
9+
- :func:`read_stdin_utf8` — read the hook payload as UTF-8 bytes
10+
regardless of locale (``sys.stdin.buffer`` bypasses the
11+
locale-encoded text wrapper, which is cp1252 on most Windows
12+
machines).
13+
- :func:`ensure_repo_src_on_path` — make the repo's ``src/``
14+
importable so hooks can lazily import ``attune.*`` without the
15+
POSIX-only ``PYTHONPATH=src python …`` env-prefix syntax in the
16+
hook registration.
17+
18+
Copyright 2026 Smart-AI-Memory
19+
Licensed under the Apache License, Version 2.0
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import sys
25+
from pathlib import Path
26+
27+
28+
def ensure_utf8_stdio() -> None:
29+
"""Reconfigure stdout/stderr to UTF-8 with replacement.
30+
31+
No-op when the streams are already UTF-8 (macOS/Linux default)
32+
or do not support ``reconfigure`` (e.g. pytest capture objects).
33+
"""
34+
for stream in (sys.stdout, sys.stderr):
35+
encoding = getattr(stream, "encoding", None)
36+
if (
37+
encoding
38+
and encoding.lower() not in ("utf-8", "utf8")
39+
and hasattr(stream, "reconfigure")
40+
):
41+
stream.reconfigure(encoding="utf-8", errors="replace")
42+
43+
44+
def read_stdin_utf8(limit: int | None = None) -> str:
45+
"""Read stdin as UTF-8 text, independent of the locale encoding.
46+
47+
Args:
48+
limit: Optional byte cap (e.g. 10_000 to bound hook input).
49+
50+
Returns:
51+
Decoded payload; undecodable bytes become U+FFFD replacements
52+
rather than raising, so a hook never crashes on odd input.
53+
"""
54+
buffer = getattr(sys.stdin, "buffer", None)
55+
if buffer is None: # already detached/wrapped (tests)
56+
return sys.stdin.read() if limit is None else sys.stdin.read(limit)
57+
data = buffer.read() if limit is None else buffer.read(limit)
58+
return data.decode("utf-8", errors="replace")
59+
60+
61+
def ensure_repo_src_on_path() -> None:
62+
"""Insert the repo's ``src/`` directory at the front of ``sys.path``.
63+
64+
Resolved relative to this file — ``parents[3]`` climbs
65+
``scripts/`` → ``hooks/`` → ``attune/`` → ``src/`` — so it works
66+
from any cwd, any worktree, and any platform without env-prefix
67+
syntax in the registration.
68+
"""
69+
try:
70+
src = Path(__file__).resolve().parents[3]
71+
except IndexError:
72+
return
73+
# Require the real package (__init__.py), not just a directory named
74+
# "attune" — from the plugin copy, parents[3] lands OUTSIDE the repo,
75+
# where an unrelated "attune" dir (e.g. a workspace umbrella checkout)
76+
# would otherwise shadow the installed package as a namespace package.
77+
if not (src / "attune" / "__init__.py").is_file(): # plugin copy / moved layout — no-op
78+
return
79+
src_str = str(src)
80+
if src_str not in sys.path:
81+
sys.path.insert(0, src_str)

.claude/hooks/_state.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,8 @@ def _run_git(cwd: Path, *args: str) -> str:
919919
cwd=str(cwd),
920920
capture_output=True,
921921
text=True,
922+
encoding="utf-8",
923+
errors="replace",
922924
timeout=2.0,
923925
check=False,
924926
)

.claude/hooks/compact_warning.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env python3
1+
#!/usr/bin/env python
22
"""Stop-hook compact-warning — fires once per session at threshold.
33
44
Stop-hook payloads from Claude Code do NOT expose a context-

.claude/hooks/format_on_save.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ def _run_formatter(cmd: list[str], path: str) -> None:
7171
def main() -> None:
7272
"""Read tool result from stdin, format Python files."""
7373
try:
74-
raw = sys.stdin.read()
74+
_buf = getattr(sys.stdin, "buffer", None) # None when tests patch stdin
75+
raw = _buf.read().decode("utf-8", errors="replace") if _buf else sys.stdin.read()
7576
if not raw.strip():
7677
return
7778

@@ -102,6 +103,15 @@ def main() -> None:
102103

103104

104105
if __name__ == "__main__":
106+
try:
107+
from _bootstrap import ensure_utf8_stdio
108+
except ImportError:
109+
# Vendored copies (sibling .claude/hooks/) may not ship
110+
# _bootstrap.py — degrade to the pre-bootstrap behavior
111+
# rather than crashing the hook.
112+
pass
113+
else:
114+
ensure_utf8_stdio()
105115
from _sdk_gate import exit_if_sdk_subprocess
106116

107117
exit_if_sdk_subprocess()

0 commit comments

Comments
 (0)