From 44dc96d2c6a96f0bd38e91ee05fc25168a3bcded Mon Sep 17 00:00:00 2001 From: Nomadu27 Date: Tue, 10 Mar 2026 17:52:44 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20address=20CodeRabbit=20review=20?= =?UTF-8?q?=E2=80=94=20convert=20to=20PreToolUse,=20add=20type=20annotatio?= =?UTF-8?q?ns,=20logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fixes: - Convert hook from PostToolUse to PreToolUse so exit(2) blocking works - Change all python references to python3 for cross-platform compat - Add insaits-security-wrapper.js to bridge run-with-flags.js to Python Standard fixes: - Wrap hook with run-with-flags.js so users can disable via ECC_DISABLED_HOOKS="pre:insaits-security" - Add "async": true to hooks.json entry - Add type annotations to all function signatures (Dict, List, Tuple, Any) - Replace all print() statements with logging module (stderr) - Fix silent OSError swallow in write_audit — now logs warning - Remove os.environ.setdefault('INSAITS_DEV_MODE') — pass dev_mode=True through monitor constructor instead - Update hooks/README.md: moved to PreToolUse table, "detects" not "catches", clarify blocking vs non-blocking behavior Co-Authored-By: Claude Opus 4.6 --- hooks/README.md | 2 +- hooks/hooks.json | 23 ++-- mcp-configs/mcp-servers.json | 2 +- scripts/hooks/insaits-security-monitor.py | 153 +++++++++++++--------- scripts/hooks/insaits-security-wrapper.js | 62 +++++++++ 5 files changed, 166 insertions(+), 76 deletions(-) create mode 100644 scripts/hooks/insaits-security-wrapper.js diff --git a/hooks/README.md b/hooks/README.md index 1fc1fb23..dde8a020 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -25,6 +25,7 @@ User request → Claude picks a tool → PreToolUse hook runs → Tool executes | **Git push reminder** | `Bash` | Reminds to review changes before `git push` | 0 (warns) | | **Doc file warning** | `Write` | Warns about non-standard `.md`/`.txt` files (allows README, CLAUDE, CONTRIBUTING, CHANGELOG, LICENSE, SKILL, docs/, skills/); cross-platform path handling | 0 (warns) | | **Strategic compact** | `Edit\|Write` | Suggests manual `/compact` at logical intervals (every ~50 tool calls) | 0 (warns) | +| **InsAIts security monitor** | `*` | Detects credential exposure, prompt injection, hallucinations, and behavioral anomalies (23 types) before tool execution. Blocks on critical findings, warns on non-critical. Writes audit log to `.insaits_audit_session.jsonl`. Requires `pip install insa-its`. [Details](../scripts/hooks/insaits-security-monitor.py) | 2 (blocks critical) / 0 (warns) | ### PostToolUse Hooks @@ -36,7 +37,6 @@ User request → Claude picks a tool → PreToolUse hook runs → Tool executes | **Prettier format** | `Edit` | Auto-formats JS/TS files with Prettier after edits | | **TypeScript check** | `Edit` | Runs `tsc --noEmit` after editing `.ts`/`.tsx` files | | **console.log warning** | `Edit` | Warns about `console.log` statements in edited files | -| **InsAIts security monitor** | `.*` | Real-time AI security: catches credential exposure, prompt injection, hallucinations, behavioral anomalies (23 types). Writes audit log to `.insaits_audit_session.jsonl`. Requires `pip install insa-its`. [Details](../scripts/hooks/insaits-security-monitor.py) | ### Lifecycle Hooks diff --git a/hooks/hooks.json b/hooks/hooks.json index 096a1ac6..3edc6778 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -63,6 +63,18 @@ } ], "description": "Capture tool use observations for continuous learning" + }, + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"pre:insaits-security\" \"scripts/hooks/insaits-security-wrapper.js\" \"standard,strict\"", + "async": true, + "timeout": 15 + } + ], + "description": "InsAIts AI security monitor: detects credential exposure, prompt injection, hallucinations, and 20+ anomaly types before tool execution. Requires: pip install insa-its" } ], "PreCompact": [ @@ -165,17 +177,6 @@ } ], "description": "Capture tool use results for continuous learning" - }, - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "python \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/insaits-security-monitor.py\"", - "timeout": 15 - } - ], - "description": "InsAIts AI security monitor: catches credential exposure, prompt injection, hallucinations, and 20+ anomaly types. Requires: pip install insa-its" } ], "Stop": [ diff --git a/mcp-configs/mcp-servers.json b/mcp-configs/mcp-servers.json index aa4044e4..64b1ad00 100644 --- a/mcp-configs/mcp-servers.json +++ b/mcp-configs/mcp-servers.json @@ -90,7 +90,7 @@ "description": "Filesystem operations (set your path)" }, "insaits": { - "command": "python", + "command": "python3", "args": ["-m", "insa_its.mcp_server"], "description": "AI-to-AI security monitoring — anomaly detection, credential exposure, hallucination checks, forensic tracing. 23 anomaly types, OWASP MCP Top 10 coverage. 100% local. Install: pip install insa-its" } diff --git a/scripts/hooks/insaits-security-monitor.py b/scripts/hooks/insaits-security-monitor.py index 97129e8f..e0a8ab14 100644 --- a/scripts/hooks/insaits-security-monitor.py +++ b/scripts/hooks/insaits-security-monitor.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 """ -InsAIts Security Monitor — PostToolUse Hook for Claude Code +InsAIts Security Monitor -- PreToolUse Hook for Claude Code ============================================================ -Real-time security monitoring for Claude Code tool outputs. -Catches credential exposure, prompt injection, behavioral anomalies, -hallucination chains, and 20+ other anomaly types — runs 100% locally. +Real-time security monitoring for Claude Code tool inputs. +Detects credential exposure, prompt injection, behavioral anomalies, +hallucination chains, and 20+ other anomaly types -- runs 100% locally. Writes audit events to .insaits_audit_session.jsonl for forensic tracing. @@ -15,13 +15,13 @@ Setup: Add to .claude/settings.json: { "hooks": { - "PostToolUse": [ + "PreToolUse": [ { "matcher": ".*", "hooks": [ { "type": "command", - "command": "python scripts/hooks/insaits-security-monitor.py" + "command": "python3 scripts/hooks/insaits-security-monitor.py" } ] } @@ -30,73 +30,97 @@ Setup: } How it works: - Claude Code passes tool result as JSON on stdin. - This script runs InsAIts anomaly detection on the output. + Claude Code passes tool input as JSON on stdin. + This script runs InsAIts anomaly detection on the content. Exit code 0 = clean (pass through). - Exit code 2 = critical issue found (blocks action, shows feedback to Claude). + Exit code 2 = critical issue found (blocks tool execution). + Stderr output = non-blocking warning shown to Claude. Detections include: - - Credential exposure (API keys, tokens, passwords in output) + - Credential exposure (API keys, tokens, passwords) - Prompt injection patterns - Hallucination indicators (phantom citations, fact contradictions) - Behavioral anomalies (context loss, semantic drift) - Tool description divergence - Shorthand emergence / jargon drift -All processing is local — no data leaves your machine. +All processing is local -- no data leaves your machine. -Author: Cristi Bogdan — YuyAI (https://github.com/Nomadu27/InsAIts) +Author: Cristi Bogdan -- YuyAI (https://github.com/Nomadu27/InsAIts) License: Apache 2.0 """ -import sys -import json -import os +from __future__ import annotations + import hashlib +import json +import logging +import os +import sys import time +from typing import Any, Dict, List, Optional, Tuple + +# Configure logging to stderr so it does not interfere with stdout protocol +logging.basicConfig( + stream=sys.stderr, + format="[InsAIts] %(message)s", + level=logging.DEBUG if os.environ.get("INSAITS_VERBOSE") else logging.WARNING, +) +log = logging.getLogger("insaits-hook") # Try importing InsAIts SDK try: from insa_its import insAItsMonitor - INSAITS_AVAILABLE = True + INSAITS_AVAILABLE: bool = True except ImportError: INSAITS_AVAILABLE = False -AUDIT_FILE = ".insaits_audit_session.jsonl" +AUDIT_FILE: str = ".insaits_audit_session.jsonl" -def extract_content(data): - """Extract inspectable text from a Claude Code tool result.""" - tool_name = data.get("tool_name", "") - tool_input = data.get("tool_input", {}) - tool_result = data.get("tool_response", {}) +def extract_content(data: Dict[str, Any]) -> Tuple[str, str]: + """Extract inspectable text from a Claude Code tool input payload. - text = "" - context = "" + Returns: + A (text, context) tuple where *text* is the content to scan and + *context* is a short label for the audit log. + """ + tool_name: str = data.get("tool_name", "") + tool_input: Dict[str, Any] = data.get("tool_input", {}) + tool_result: Any = data.get("tool_response", {}) + + text: str = "" + context: str = "" if tool_name in ("Write", "Edit", "MultiEdit"): text = tool_input.get("content", "") or tool_input.get("new_string", "") - context = "file:" + tool_input.get("file_path", "")[:80] + context = "file:" + str(tool_input.get("file_path", ""))[:80] elif tool_name == "Bash": + command: str = str(tool_input.get("command", "")) + # For PreToolUse we inspect the command itself + text = command + # Also check tool_response if present (for flexibility) if isinstance(tool_result, dict): - text = tool_result.get("output", "") or tool_result.get("stdout", "") - elif isinstance(tool_result, str): - text = tool_result - context = "bash:" + str(tool_input.get("command", ""))[:80] + output = tool_result.get("output", "") or tool_result.get("stdout", "") + if output: + text = text + "\n" + output + elif isinstance(tool_result, str) and tool_result: + text = text + "\n" + tool_result + context = "bash:" + command[:80] elif "content" in data: - content = data["content"] + content: Any = data["content"] if isinstance(content, list): text = "\n".join( b.get("text", "") for b in content if b.get("type") == "text" ) elif isinstance(content, str): text = content - context = data.get("task", "") + context = str(data.get("task", "")) return text, context -def write_audit(event): +def write_audit(event: Dict[str, Any]) -> None: """Append an audit event to the JSONL audit log.""" try: event["timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) @@ -105,20 +129,24 @@ def write_audit(event): ).hexdigest()[:16] with open(AUDIT_FILE, "a", encoding="utf-8") as f: f.write(json.dumps(event) + "\n") - except OSError: - pass + except OSError as exc: + log.warning("Failed to write audit log %s: %s", AUDIT_FILE, exc) -def format_feedback(anomalies): - """Format detected anomalies as feedback for Claude Code.""" - lines = [ - "== InsAIts Security Monitor — Issues Detected ==", +def format_feedback(anomalies: List[Any]) -> str: + """Format detected anomalies as feedback for Claude Code. + + Returns: + A human-readable multi-line string describing each finding. + """ + lines: List[str] = [ + "== InsAIts Security Monitor -- Issues Detected ==", "", ] for i, a in enumerate(anomalies, 1): - sev = getattr(a, "severity", "MEDIUM") - atype = getattr(a, "type", "UNKNOWN") - detail = getattr(a, "detail", "") + sev: str = getattr(a, "severity", "MEDIUM") + atype: str = getattr(a, "type", "UNKNOWN") + detail: str = getattr(a, "detail", "") lines.extend([ f"{i}. [{sev}] {atype}", f" {detail[:120]}", @@ -132,40 +160,38 @@ def format_feedback(anomalies): return "\n".join(lines) -def main(): - raw = sys.stdin.read().strip() +def main() -> None: + """Entry point for the Claude Code PreToolUse hook.""" + raw: str = sys.stdin.read().strip() if not raw: sys.exit(0) try: - data = json.loads(raw) + data: Dict[str, Any] = json.loads(raw) except json.JSONDecodeError: data = {"content": raw} text, context = extract_content(data) - # Skip very short or binary content + # Skip very short content (e.g. "OK", empty bash results) if len(text.strip()) < 10: sys.exit(0) if not INSAITS_AVAILABLE: - print( - "[InsAIts] Not installed. Run: pip install insa-its", - file=sys.stderr, - ) + log.warning("Not installed. Run: pip install insa-its") sys.exit(0) - # Enable dev mode (no API key needed for local detection) - os.environ.setdefault("INSAITS_DEV_MODE", "true") - - monitor = insAItsMonitor(session_name="claude-code-hook") - result = monitor.send_message( + monitor: insAItsMonitor = insAItsMonitor( + session_name="claude-code-hook", + dev_mode=True, + ) + result: Dict[str, Any] = monitor.send_message( text=text[:4000], sender_id="claude-code", llm_id=os.environ.get("INSAITS_MODEL", "claude-opus"), ) - anomalies = result.get("anomalies", []) + anomalies: List[Any] = result.get("anomalies", []) # Write audit event regardless of findings write_audit({ @@ -177,22 +203,23 @@ def main(): }) if not anomalies: - if os.environ.get("INSAITS_VERBOSE"): - print("[InsAIts] Clean — no anomalies.", file=sys.stderr) + log.debug("Clean -- no anomalies detected.") sys.exit(0) - # Check severity - has_critical = any( + # Determine maximum severity + has_critical: bool = any( getattr(a, "severity", "") in ("CRITICAL", "critical") for a in anomalies ) - feedback = format_feedback(anomalies) + feedback: str = format_feedback(anomalies) if has_critical: - print(feedback) # stdout -> Claude Code shows to model - sys.exit(2) # block action + # stdout feedback -> Claude Code shows to the model + sys.stdout.write(feedback + "\n") + sys.exit(2) # PreToolUse exit 2 = block tool execution else: - print(feedback, file=sys.stderr) # stderr -> logged only + # Non-critical: warn via stderr (non-blocking) + log.warning("\n%s", feedback) sys.exit(0) diff --git a/scripts/hooks/insaits-security-wrapper.js b/scripts/hooks/insaits-security-wrapper.js new file mode 100644 index 00000000..6223b368 --- /dev/null +++ b/scripts/hooks/insaits-security-wrapper.js @@ -0,0 +1,62 @@ +#!/usr/bin/env node +/** + * InsAIts Security Monitor — wrapper for run-with-flags compatibility. + * + * This thin wrapper receives stdin from the hooks infrastructure and + * delegates to the Python-based insaits-security-monitor.py script. + * + * The wrapper exists because run-with-flags.js spawns child scripts + * via `node`, so a JS entry point is needed to bridge to Python. + */ + +'use strict'; + +const path = require('path'); +const { spawnSync } = require('child_process'); + +const MAX_STDIN = 1024 * 1024; + +let raw = ''; +process.stdin.setEncoding('utf8'); +process.stdin.on('data', chunk => { + if (raw.length < MAX_STDIN) { + raw += chunk.substring(0, MAX_STDIN - raw.length); + } +}); + +process.stdin.on('end', () => { + const scriptDir = __dirname; + const pyScript = path.join(scriptDir, 'insaits-security-monitor.py'); + + // Try python3 first (macOS/Linux), fall back to python (Windows) + const pythonCandidates = ['python3', 'python']; + let result; + + for (const pythonBin of pythonCandidates) { + result = spawnSync(pythonBin, [pyScript], { + input: raw, + encoding: 'utf8', + env: process.env, + cwd: process.cwd(), + timeout: 14000, + }); + + // ENOENT means binary not found — try next candidate + if (result.error && result.error.code === 'ENOENT') { + continue; + } + break; + } + + if (!result || (result.error && result.error.code === 'ENOENT')) { + process.stderr.write('[InsAIts] python3/python not found. Install Python 3.9+ and: pip install insa-its\n'); + process.stdout.write(raw); + process.exit(0); + } + + if (result.stdout) process.stdout.write(result.stdout); + if (result.stderr) process.stderr.write(result.stderr); + + const code = Number.isInteger(result.status) ? result.status : 0; + process.exit(code); +});