mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-16 13:15:36 +02:00
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.
80 lines
2.5 KiB
Python
Executable File
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()) |