Files
ECC/hooks/hermes/check-console-log.py
T
ravitejarenangi b30a8c782a fix(hooks): address review — close bypasses, surface failures, add test suite
Greptile + CodeRabbit review of #3019:

- config-protection: compare basenames case-insensitively (macOS/Windows
  resolve ESLINT.CONFIG.JS to the protected eslint.config.js) and use lstat
  so a dangling symlink to a protected name still blocks (os.path.exists
  follows the link and returns False).
- Both hooks: fail-open stays, silence goes — every swallowed path now
  writes a one-line stderr diagnostic.
- config-protection: sys.stdout.write instead of print() per repo Python
  style; block decisions carry a trailing newline.
- New tests/hermes-hooks.test.js: 13 cases through the real scripts via
  spawnSync — block (all names, case variants, dangling symlink, edits[],
  per-tool), allow (normal source, new config, non-edit tools), fail-open
  with diagnostics (malformed payload, missing path, unreadable file).
- docs: absolute-path copy-paste block via $HOME (CodeRabbit: /Users/YOU
  placeholders were unverifiable), self-contained block-case proof that
  creates its target, pointer to the committed suite.
2026-09-08 12:38:29 +05:30

80 lines
2.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Hermes native hook (ported from ECC's scripts/hooks/check-console-log.js).
post_tool_call observer: after any file edit, WARN (never block) when the
edited file now contains console.log(). Print to stderr only — Hermes shows
hook stderr as a warning line.
Contract (agent/shell_hooks.py):
stdin : JSON payload {"hook_event_name":"post_tool_call","tool_name":"...",
"tool_input":{...},"tool_result":{...}}
exit : 0 (observer, no stdout JSON)
Error handling: fail-open by design (observers must not brick the agent),
but never silent — every swallowed path writes a one-line stderr diagnostic.
"""
import json
import os
import re
import sys
EDIT_TOOLS = {"patch", "write_file", "edit", "write", "apply_patch", "str_replace_editor"}
EXCLUDE = re.compile(r"(\.d\.ts$|/__tests?__/|\.test\.[tj]sx?$|\.spec\.[tj]sx?$|/scripts/|console-safety)")
def _warn(msg: str) -> None:
sys.stderr.write(f"[ecc-check-console-log] {msg}\n")
def main() -> int:
raw = sys.stdin.read()
try:
payload = json.loads(raw) if raw.strip() else {}
if not isinstance(payload, dict):
_warn(f"payload is {type(payload).__name__}, not an object — skipping")
return 0
except ValueError as exc:
_warn(f"unparseable payload ({exc}) — skipping")
return 0
tool = str(payload.get("tool_name") or "")
if tool not in EDIT_TOOLS:
return 0
ti = payload.get("tool_input") or {}
if not isinstance(ti, dict):
_warn("tool_input is not an object — skipping")
return 0
path = None
for key in ("path", "file_path", "filePath", "target"):
v = ti.get(key)
if isinstance(v, str) and v.strip():
path = v.strip()
break
if not path:
_warn(f"no path found in tool_input for tool {tool!r} — skipping")
return 0
if not re.search(r"\.[tj]sx?$", path) or EXCLUDE.search(path):
return 0
if not os.path.isfile(path):
_warn(f"{path} is not a regular file on disk — skipping")
return 0
try:
with open(path, "r", errors="replace") as f:
content = f.read()
except OSError as exc:
_warn(f"could not read {path} ({exc.strerror or exc}) — skipping")
return 0
if "console.log(" in content:
_warn(
f"console.log() present in {path} — remove it before committing "
"(use the project logger instead)."
)
return 0
if __name__ == "__main__":
sys.exit(main())