cc4pm 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/README.md +17 -0
- package/.claude-plugin/plugin.json +25 -0
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/README.zh-CN.md +134 -0
- package/contexts/dev.md +20 -0
- package/contexts/research.md +26 -0
- package/contexts/review.md +22 -0
- package/examples/CLAUDE.md +100 -0
- package/examples/statusline.json +19 -0
- package/examples/user-CLAUDE.md +109 -0
- package/install.sh +17 -0
- package/manifests/install-components.json +173 -0
- package/manifests/install-modules.json +335 -0
- package/manifests/install-profiles.json +75 -0
- package/package.json +117 -0
- package/schemas/ecc-install-config.schema.json +58 -0
- package/schemas/hooks.schema.json +197 -0
- package/schemas/install-components.schema.json +56 -0
- package/schemas/install-modules.schema.json +105 -0
- package/schemas/install-profiles.schema.json +45 -0
- package/schemas/install-state.schema.json +210 -0
- package/schemas/package-manager.schema.json +23 -0
- package/schemas/plugin.schema.json +58 -0
- package/scripts/ci/catalog.js +83 -0
- package/scripts/ci/validate-agents.js +81 -0
- package/scripts/ci/validate-commands.js +135 -0
- package/scripts/ci/validate-hooks.js +239 -0
- package/scripts/ci/validate-install-manifests.js +211 -0
- package/scripts/ci/validate-no-personal-paths.js +63 -0
- package/scripts/ci/validate-rules.js +81 -0
- package/scripts/ci/validate-skills.js +54 -0
- package/scripts/claw.js +468 -0
- package/scripts/doctor.js +110 -0
- package/scripts/ecc.js +194 -0
- package/scripts/hooks/auto-tmux-dev.js +88 -0
- package/scripts/hooks/check-console-log.js +71 -0
- package/scripts/hooks/check-hook-enabled.js +12 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +100 -0
- package/scripts/hooks/insaits-security-monitor.py +269 -0
- package/scripts/hooks/insaits-security-wrapper.js +88 -0
- package/scripts/hooks/post-bash-build-complete.js +27 -0
- package/scripts/hooks/post-bash-pr-created.js +36 -0
- package/scripts/hooks/post-edit-console-warn.js +54 -0
- package/scripts/hooks/post-edit-format.js +109 -0
- package/scripts/hooks/post-edit-typecheck.js +96 -0
- package/scripts/hooks/pre-bash-dev-server-block.js +187 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +28 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +33 -0
- package/scripts/hooks/pre-compact.js +48 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +168 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +120 -0
- package/scripts/hooks/session-end-marker.js +15 -0
- package/scripts/hooks/session-end.js +299 -0
- package/scripts/hooks/session-start.js +97 -0
- package/scripts/hooks/suggest-compact.js +80 -0
- package/scripts/install-apply.js +137 -0
- package/scripts/install-plan.js +254 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install/apply.js +23 -0
- package/scripts/lib/install/config.js +82 -0
- package/scripts/lib/install/request.js +113 -0
- package/scripts/lib/install/runtime.js +42 -0
- package/scripts/lib/install-executor.js +605 -0
- package/scripts/lib/install-lifecycle.js +763 -0
- package/scripts/lib/install-manifests.js +305 -0
- package/scripts/lib/install-state.js +120 -0
- package/scripts/lib/install-targets/antigravity-project.js +9 -0
- package/scripts/lib/install-targets/claude-home.js +10 -0
- package/scripts/lib/install-targets/codex-home.js +10 -0
- package/scripts/lib/install-targets/cursor-project.js +10 -0
- package/scripts/lib/install-targets/helpers.js +89 -0
- package/scripts/lib/install-targets/opencode-home.js +10 -0
- package/scripts/lib/install-targets/registry.js +64 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.d.ts +119 -0
- package/scripts/lib/package-manager.js +431 -0
- package/scripts/lib/project-detect.js +428 -0
- package/scripts/lib/resolve-formatter.js +185 -0
- package/scripts/lib/session-adapters/canonical-session.js +138 -0
- package/scripts/lib/session-adapters/claude-history.js +149 -0
- package/scripts/lib/session-adapters/dmux-tmux.js +80 -0
- package/scripts/lib/session-adapters/registry.js +111 -0
- package/scripts/lib/session-aliases.d.ts +136 -0
- package/scripts/lib/session-aliases.js +481 -0
- package/scripts/lib/session-manager.d.ts +131 -0
- package/scripts/lib/session-manager.js +464 -0
- package/scripts/lib/shell-split.js +86 -0
- package/scripts/lib/skill-improvement/amendify.js +89 -0
- package/scripts/lib/skill-improvement/evaluate.js +59 -0
- package/scripts/lib/skill-improvement/health.js +118 -0
- package/scripts/lib/skill-improvement/observations.js +108 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +491 -0
- package/scripts/lib/utils.d.ts +183 -0
- package/scripts/lib/utils.js +543 -0
- package/scripts/list-installed.js +90 -0
- package/scripts/orchestrate-codex-worker.sh +92 -0
- package/scripts/orchestrate-worktrees.js +108 -0
- package/scripts/orchestration-status.js +62 -0
- package/scripts/repair.js +97 -0
- package/scripts/session-inspect.js +150 -0
- package/scripts/setup-package-manager.js +204 -0
- package/scripts/skill-create-output.js +244 -0
- package/scripts/uninstall.js +96 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
InsAIts Security Monitor -- PreToolUse Hook for Claude Code
|
|
4
|
+
============================================================
|
|
5
|
+
|
|
6
|
+
Real-time security monitoring for Claude Code tool inputs.
|
|
7
|
+
Detects credential exposure, prompt injection, behavioral anomalies,
|
|
8
|
+
hallucination chains, and 20+ other anomaly types -- runs 100% locally.
|
|
9
|
+
|
|
10
|
+
Writes audit events to .insaits_audit_session.jsonl for forensic tracing.
|
|
11
|
+
|
|
12
|
+
Setup:
|
|
13
|
+
pip install insa-its
|
|
14
|
+
export ECC_ENABLE_INSAITS=1
|
|
15
|
+
|
|
16
|
+
Add to .claude/settings.json:
|
|
17
|
+
{
|
|
18
|
+
"hooks": {
|
|
19
|
+
"PreToolUse": [
|
|
20
|
+
{
|
|
21
|
+
"matcher": "Bash|Write|Edit|MultiEdit",
|
|
22
|
+
"hooks": [
|
|
23
|
+
{
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": "node scripts/hooks/insaits-security-wrapper.js"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
How it works:
|
|
34
|
+
Claude Code passes tool input as JSON on stdin.
|
|
35
|
+
This script runs InsAIts anomaly detection on the content.
|
|
36
|
+
Exit code 0 = clean (pass through).
|
|
37
|
+
Exit code 2 = critical issue found (blocks tool execution).
|
|
38
|
+
Stderr output = non-blocking warning shown to Claude.
|
|
39
|
+
|
|
40
|
+
Environment variables:
|
|
41
|
+
INSAITS_DEV_MODE Set to "true" to enable dev mode (no API key needed).
|
|
42
|
+
Defaults to "false" (strict mode).
|
|
43
|
+
INSAITS_MODEL LLM model identifier for fingerprinting. Default: claude-opus.
|
|
44
|
+
INSAITS_FAIL_MODE "open" (default) = continue on SDK errors.
|
|
45
|
+
"closed" = block tool execution on SDK errors.
|
|
46
|
+
INSAITS_VERBOSE Set to any value to enable debug logging.
|
|
47
|
+
|
|
48
|
+
Detections include:
|
|
49
|
+
- Credential exposure (API keys, tokens, passwords)
|
|
50
|
+
- Prompt injection patterns
|
|
51
|
+
- Hallucination indicators (phantom citations, fact contradictions)
|
|
52
|
+
- Behavioral anomalies (context loss, semantic drift)
|
|
53
|
+
- Tool description divergence
|
|
54
|
+
- Shorthand emergence / jargon drift
|
|
55
|
+
|
|
56
|
+
All processing is local -- no data leaves your machine.
|
|
57
|
+
|
|
58
|
+
Author: Cristi Bogdan -- YuyAI (https://github.com/Nomadu27/InsAIts)
|
|
59
|
+
License: Apache 2.0
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
from __future__ import annotations
|
|
63
|
+
|
|
64
|
+
import hashlib
|
|
65
|
+
import json
|
|
66
|
+
import logging
|
|
67
|
+
import os
|
|
68
|
+
import sys
|
|
69
|
+
import time
|
|
70
|
+
from typing import Any, Dict, List, Tuple
|
|
71
|
+
|
|
72
|
+
# Configure logging to stderr so it does not interfere with stdout protocol
|
|
73
|
+
logging.basicConfig(
|
|
74
|
+
stream=sys.stderr,
|
|
75
|
+
format="[InsAIts] %(message)s",
|
|
76
|
+
level=logging.DEBUG if os.environ.get("INSAITS_VERBOSE") else logging.WARNING,
|
|
77
|
+
)
|
|
78
|
+
log = logging.getLogger("insaits-hook")
|
|
79
|
+
|
|
80
|
+
# Try importing InsAIts SDK
|
|
81
|
+
try:
|
|
82
|
+
from insa_its import insAItsMonitor
|
|
83
|
+
INSAITS_AVAILABLE: bool = True
|
|
84
|
+
except ImportError:
|
|
85
|
+
INSAITS_AVAILABLE = False
|
|
86
|
+
|
|
87
|
+
# --- Constants ---
|
|
88
|
+
AUDIT_FILE: str = ".insaits_audit_session.jsonl"
|
|
89
|
+
MIN_CONTENT_LENGTH: int = 10
|
|
90
|
+
MAX_SCAN_LENGTH: int = 4000
|
|
91
|
+
DEFAULT_MODEL: str = "claude-opus"
|
|
92
|
+
BLOCKING_SEVERITIES: frozenset = frozenset({"CRITICAL"})
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_content(data: Dict[str, Any]) -> Tuple[str, str]:
|
|
96
|
+
"""Extract inspectable text from a Claude Code tool input payload.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A (text, context) tuple where *text* is the content to scan and
|
|
100
|
+
*context* is a short label for the audit log.
|
|
101
|
+
"""
|
|
102
|
+
tool_name: str = data.get("tool_name", "")
|
|
103
|
+
tool_input: Dict[str, Any] = data.get("tool_input", {})
|
|
104
|
+
|
|
105
|
+
text: str = ""
|
|
106
|
+
context: str = ""
|
|
107
|
+
|
|
108
|
+
if tool_name in ("Write", "Edit", "MultiEdit"):
|
|
109
|
+
text = tool_input.get("content", "") or tool_input.get("new_string", "")
|
|
110
|
+
context = "file:" + str(tool_input.get("file_path", ""))[:80]
|
|
111
|
+
elif tool_name == "Bash":
|
|
112
|
+
# PreToolUse: the tool hasn't executed yet, inspect the command
|
|
113
|
+
command: str = str(tool_input.get("command", ""))
|
|
114
|
+
text = command
|
|
115
|
+
context = "bash:" + command[:80]
|
|
116
|
+
elif "content" in data:
|
|
117
|
+
content: Any = data["content"]
|
|
118
|
+
if isinstance(content, list):
|
|
119
|
+
text = "\n".join(
|
|
120
|
+
b.get("text", "") for b in content if b.get("type") == "text"
|
|
121
|
+
)
|
|
122
|
+
elif isinstance(content, str):
|
|
123
|
+
text = content
|
|
124
|
+
context = str(data.get("task", ""))
|
|
125
|
+
|
|
126
|
+
return text, context
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def write_audit(event: Dict[str, Any]) -> None:
|
|
130
|
+
"""Append an audit event to the JSONL audit log.
|
|
131
|
+
|
|
132
|
+
Creates a new dict to avoid mutating the caller's *event*.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
enriched: Dict[str, Any] = {
|
|
136
|
+
**event,
|
|
137
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
138
|
+
}
|
|
139
|
+
enriched["hash"] = hashlib.sha256(
|
|
140
|
+
json.dumps(enriched, sort_keys=True).encode()
|
|
141
|
+
).hexdigest()[:16]
|
|
142
|
+
with open(AUDIT_FILE, "a", encoding="utf-8") as f:
|
|
143
|
+
f.write(json.dumps(enriched) + "\n")
|
|
144
|
+
except OSError as exc:
|
|
145
|
+
log.warning("Failed to write audit log %s: %s", AUDIT_FILE, exc)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_anomaly_attr(anomaly: Any, key: str, default: str = "") -> str:
|
|
149
|
+
"""Get a field from an anomaly that may be a dict or an object.
|
|
150
|
+
|
|
151
|
+
The SDK's ``send_message()`` returns anomalies as dicts, while
|
|
152
|
+
other code paths may return dataclass/object instances. This
|
|
153
|
+
helper handles both transparently.
|
|
154
|
+
"""
|
|
155
|
+
if isinstance(anomaly, dict):
|
|
156
|
+
return str(anomaly.get(key, default))
|
|
157
|
+
return str(getattr(anomaly, key, default))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def format_feedback(anomalies: List[Any]) -> str:
|
|
161
|
+
"""Format detected anomalies as feedback for Claude Code.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
A human-readable multi-line string describing each finding.
|
|
165
|
+
"""
|
|
166
|
+
lines: List[str] = [
|
|
167
|
+
"== InsAIts Security Monitor -- Issues Detected ==",
|
|
168
|
+
"",
|
|
169
|
+
]
|
|
170
|
+
for i, a in enumerate(anomalies, 1):
|
|
171
|
+
sev: str = get_anomaly_attr(a, "severity", "MEDIUM")
|
|
172
|
+
atype: str = get_anomaly_attr(a, "type", "UNKNOWN")
|
|
173
|
+
detail: str = get_anomaly_attr(a, "details", "")
|
|
174
|
+
lines.extend([
|
|
175
|
+
f"{i}. [{sev}] {atype}",
|
|
176
|
+
f" {detail[:120]}",
|
|
177
|
+
"",
|
|
178
|
+
])
|
|
179
|
+
lines.extend([
|
|
180
|
+
"-" * 56,
|
|
181
|
+
"Fix the issues above before continuing.",
|
|
182
|
+
"Audit log: " + AUDIT_FILE,
|
|
183
|
+
])
|
|
184
|
+
return "\n".join(lines)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def main() -> None:
|
|
188
|
+
"""Entry point for the Claude Code PreToolUse hook."""
|
|
189
|
+
raw: str = sys.stdin.read().strip()
|
|
190
|
+
if not raw:
|
|
191
|
+
sys.exit(0)
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
data: Dict[str, Any] = json.loads(raw)
|
|
195
|
+
except json.JSONDecodeError:
|
|
196
|
+
data = {"content": raw}
|
|
197
|
+
|
|
198
|
+
text, context = extract_content(data)
|
|
199
|
+
|
|
200
|
+
# Skip very short content (e.g. "OK", empty bash results)
|
|
201
|
+
if len(text.strip()) < MIN_CONTENT_LENGTH:
|
|
202
|
+
sys.exit(0)
|
|
203
|
+
|
|
204
|
+
if not INSAITS_AVAILABLE:
|
|
205
|
+
log.warning("Not installed. Run: pip install insa-its")
|
|
206
|
+
sys.exit(0)
|
|
207
|
+
|
|
208
|
+
# Wrap SDK calls so an internal error does not crash the hook
|
|
209
|
+
try:
|
|
210
|
+
monitor: insAItsMonitor = insAItsMonitor(
|
|
211
|
+
session_name="claude-code-hook",
|
|
212
|
+
dev_mode=os.environ.get(
|
|
213
|
+
"INSAITS_DEV_MODE", "false"
|
|
214
|
+
).lower() in ("1", "true", "yes"),
|
|
215
|
+
)
|
|
216
|
+
result: Dict[str, Any] = monitor.send_message(
|
|
217
|
+
text=text[:MAX_SCAN_LENGTH],
|
|
218
|
+
sender_id="claude-code",
|
|
219
|
+
llm_id=os.environ.get("INSAITS_MODEL", DEFAULT_MODEL),
|
|
220
|
+
)
|
|
221
|
+
except Exception as exc: # Broad catch intentional: unknown SDK internals
|
|
222
|
+
fail_mode: str = os.environ.get("INSAITS_FAIL_MODE", "open").lower()
|
|
223
|
+
if fail_mode == "closed":
|
|
224
|
+
sys.stdout.write(
|
|
225
|
+
f"InsAIts SDK error ({type(exc).__name__}); "
|
|
226
|
+
"blocking execution to avoid unscanned input.\n"
|
|
227
|
+
)
|
|
228
|
+
sys.exit(2)
|
|
229
|
+
log.warning(
|
|
230
|
+
"SDK error (%s), skipping security scan: %s",
|
|
231
|
+
type(exc).__name__, exc,
|
|
232
|
+
)
|
|
233
|
+
sys.exit(0)
|
|
234
|
+
|
|
235
|
+
anomalies: List[Any] = result.get("anomalies", [])
|
|
236
|
+
|
|
237
|
+
# Write audit event regardless of findings
|
|
238
|
+
write_audit({
|
|
239
|
+
"tool": data.get("tool_name", "unknown"),
|
|
240
|
+
"context": context,
|
|
241
|
+
"anomaly_count": len(anomalies),
|
|
242
|
+
"anomaly_types": [get_anomaly_attr(a, "type") for a in anomalies],
|
|
243
|
+
"text_length": len(text),
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
if not anomalies:
|
|
247
|
+
log.debug("Clean -- no anomalies detected.")
|
|
248
|
+
sys.exit(0)
|
|
249
|
+
|
|
250
|
+
# Determine maximum severity
|
|
251
|
+
has_critical: bool = any(
|
|
252
|
+
get_anomaly_attr(a, "severity").upper() in BLOCKING_SEVERITIES
|
|
253
|
+
for a in anomalies
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
feedback: str = format_feedback(anomalies)
|
|
257
|
+
|
|
258
|
+
if has_critical:
|
|
259
|
+
# stdout feedback -> Claude Code shows to the model
|
|
260
|
+
sys.stdout.write(feedback + "\n")
|
|
261
|
+
sys.exit(2) # PreToolUse exit 2 = block tool execution
|
|
262
|
+
else:
|
|
263
|
+
# Non-critical: warn via stderr (non-blocking)
|
|
264
|
+
log.warning("\n%s", feedback)
|
|
265
|
+
sys.exit(0)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* InsAIts Security Monitor — wrapper for run-with-flags compatibility.
|
|
4
|
+
*
|
|
5
|
+
* This thin wrapper receives stdin from the hooks infrastructure and
|
|
6
|
+
* delegates to the Python-based insaits-security-monitor.py script.
|
|
7
|
+
*
|
|
8
|
+
* The wrapper exists because run-with-flags.js spawns child scripts
|
|
9
|
+
* via `node`, so a JS entry point is needed to bridge to Python.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { spawnSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
const MAX_STDIN = 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
function isEnabled(value) {
|
|
20
|
+
return ['1', 'true', 'yes', 'on'].includes(String(value || '').toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let raw = '';
|
|
24
|
+
process.stdin.setEncoding('utf8');
|
|
25
|
+
process.stdin.on('data', chunk => {
|
|
26
|
+
if (raw.length < MAX_STDIN) {
|
|
27
|
+
raw += chunk.substring(0, MAX_STDIN - raw.length);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
process.stdin.on('end', () => {
|
|
32
|
+
if (!isEnabled(process.env.ECC_ENABLE_INSAITS)) {
|
|
33
|
+
process.stdout.write(raw);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const scriptDir = __dirname;
|
|
38
|
+
const pyScript = path.join(scriptDir, 'insaits-security-monitor.py');
|
|
39
|
+
|
|
40
|
+
// Try python3 first (macOS/Linux), fall back to python (Windows)
|
|
41
|
+
const pythonCandidates = ['python3', 'python'];
|
|
42
|
+
let result;
|
|
43
|
+
|
|
44
|
+
for (const pythonBin of pythonCandidates) {
|
|
45
|
+
result = spawnSync(pythonBin, [pyScript], {
|
|
46
|
+
input: raw,
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
env: process.env,
|
|
49
|
+
cwd: process.cwd(),
|
|
50
|
+
timeout: 14000,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ENOENT means binary not found — try next candidate
|
|
54
|
+
if (result.error && result.error.code === 'ENOENT') {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!result || (result.error && result.error.code === 'ENOENT')) {
|
|
61
|
+
process.stderr.write('[InsAIts] python3/python not found. Install Python 3.9+ and: pip install insa-its\n');
|
|
62
|
+
process.stdout.write(raw);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Log non-ENOENT spawn errors (timeout, signal kill, etc.) so users
|
|
67
|
+
// know the security monitor did not run — fail-open with a warning.
|
|
68
|
+
if (result.error) {
|
|
69
|
+
process.stderr.write(`[InsAIts] Security monitor failed to run: ${result.error.message}\n`);
|
|
70
|
+
process.stdout.write(raw);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// result.status is null when the process was killed by a signal or
|
|
75
|
+
// timed out. Check BEFORE writing stdout to avoid leaking partial
|
|
76
|
+
// or corrupt monitor output. Pass through original raw input instead.
|
|
77
|
+
if (!Number.isInteger(result.status)) {
|
|
78
|
+
const signal = result.signal || 'unknown';
|
|
79
|
+
process.stderr.write(`[InsAIts] Security monitor killed (signal: ${signal}). Tool execution continues.\n`);
|
|
80
|
+
process.stdout.write(raw);
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
85
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
86
|
+
|
|
87
|
+
process.exit(result.status);
|
|
88
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const MAX_STDIN = 1024 * 1024;
|
|
5
|
+
let raw = '';
|
|
6
|
+
|
|
7
|
+
process.stdin.setEncoding('utf8');
|
|
8
|
+
process.stdin.on('data', chunk => {
|
|
9
|
+
if (raw.length < MAX_STDIN) {
|
|
10
|
+
const remaining = MAX_STDIN - raw.length;
|
|
11
|
+
raw += chunk.substring(0, remaining);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
process.stdin.on('end', () => {
|
|
16
|
+
try {
|
|
17
|
+
const input = JSON.parse(raw);
|
|
18
|
+
const cmd = String(input.tool_input?.command || '');
|
|
19
|
+
if (/(npm run build|pnpm build|yarn build)/.test(cmd)) {
|
|
20
|
+
console.error('[Hook] Build completed - async analysis running in background');
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// ignore parse errors and pass through
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
process.stdout.write(raw);
|
|
27
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const MAX_STDIN = 1024 * 1024;
|
|
5
|
+
let raw = '';
|
|
6
|
+
|
|
7
|
+
process.stdin.setEncoding('utf8');
|
|
8
|
+
process.stdin.on('data', chunk => {
|
|
9
|
+
if (raw.length < MAX_STDIN) {
|
|
10
|
+
const remaining = MAX_STDIN - raw.length;
|
|
11
|
+
raw += chunk.substring(0, remaining);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
process.stdin.on('end', () => {
|
|
16
|
+
try {
|
|
17
|
+
const input = JSON.parse(raw);
|
|
18
|
+
const cmd = String(input.tool_input?.command || '');
|
|
19
|
+
|
|
20
|
+
if (/\bgh\s+pr\s+create\b/.test(cmd)) {
|
|
21
|
+
const out = String(input.tool_output?.output || '');
|
|
22
|
+
const match = out.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/);
|
|
23
|
+
if (match) {
|
|
24
|
+
const prUrl = match[0];
|
|
25
|
+
const repo = prUrl.replace(/https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/, '$1');
|
|
26
|
+
const prNum = prUrl.replace(/.+\/pull\/(\d+)/, '$1');
|
|
27
|
+
console.error(`[Hook] PR created: ${prUrl}`);
|
|
28
|
+
console.error(`[Hook] To review: gh pr review ${prNum} --repo ${repo}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// ignore parse errors and pass through
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process.stdout.write(raw);
|
|
36
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse Hook: Warn about console.log statements after edits
|
|
4
|
+
*
|
|
5
|
+
* Cross-platform (Windows, macOS, Linux)
|
|
6
|
+
*
|
|
7
|
+
* Runs after Edit tool use. If the edited JS/TS file contains console.log
|
|
8
|
+
* statements, warns with line numbers to help remove debug statements
|
|
9
|
+
* before committing.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { readFile } = require('../lib/utils');
|
|
13
|
+
|
|
14
|
+
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
|
15
|
+
let data = '';
|
|
16
|
+
process.stdin.setEncoding('utf8');
|
|
17
|
+
|
|
18
|
+
process.stdin.on('data', chunk => {
|
|
19
|
+
if (data.length < MAX_STDIN) {
|
|
20
|
+
const remaining = MAX_STDIN - data.length;
|
|
21
|
+
data += chunk.substring(0, remaining);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
process.stdin.on('end', () => {
|
|
26
|
+
try {
|
|
27
|
+
const input = JSON.parse(data);
|
|
28
|
+
const filePath = input.tool_input?.file_path;
|
|
29
|
+
|
|
30
|
+
if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) {
|
|
31
|
+
const content = readFile(filePath);
|
|
32
|
+
if (!content) { process.stdout.write(data); process.exit(0); }
|
|
33
|
+
const lines = content.split('\n');
|
|
34
|
+
const matches = [];
|
|
35
|
+
|
|
36
|
+
lines.forEach((line, idx) => {
|
|
37
|
+
if (/console\.log/.test(line)) {
|
|
38
|
+
matches.push((idx + 1) + ': ' + line.trim());
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (matches.length > 0) {
|
|
43
|
+
console.error('[Hook] WARNING: console.log found in ' + filePath);
|
|
44
|
+
matches.slice(0, 5).forEach(m => console.error(m));
|
|
45
|
+
console.error('[Hook] Remove console.log before committing');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Invalid input — pass through
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
process.stdout.write(data);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse Hook: Auto-format JS/TS files after edits
|
|
4
|
+
*
|
|
5
|
+
* Cross-platform (Windows, macOS, Linux)
|
|
6
|
+
*
|
|
7
|
+
* Runs after Edit tool use. If the edited file is a JS/TS file,
|
|
8
|
+
* auto-detects the project formatter (Biome or Prettier) by looking
|
|
9
|
+
* for config files, then formats accordingly.
|
|
10
|
+
*
|
|
11
|
+
* For Biome, uses `check --write` (format + lint in one pass) to
|
|
12
|
+
* avoid a redundant second invocation from quality-gate.js.
|
|
13
|
+
*
|
|
14
|
+
* Prefers the local node_modules/.bin binary over npx to skip
|
|
15
|
+
* package-resolution overhead (~200-500ms savings per invocation).
|
|
16
|
+
*
|
|
17
|
+
* Fails silently if no formatter is found or installed.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
// Shell metacharacters that cmd.exe interprets as command separators/operators
|
|
24
|
+
const UNSAFE_PATH_CHARS = /[&|<>^%!]/;
|
|
25
|
+
|
|
26
|
+
const { findProjectRoot, detectFormatter, resolveFormatterBin } = require('../lib/resolve-formatter');
|
|
27
|
+
|
|
28
|
+
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Core logic — exported so run-with-flags.js can call directly
|
|
32
|
+
* without spawning a child process.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} rawInput - Raw JSON string from stdin
|
|
35
|
+
* @returns {string} The original input (pass-through)
|
|
36
|
+
*/
|
|
37
|
+
function run(rawInput) {
|
|
38
|
+
try {
|
|
39
|
+
const input = JSON.parse(rawInput);
|
|
40
|
+
const filePath = input.tool_input?.file_path;
|
|
41
|
+
|
|
42
|
+
if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) {
|
|
43
|
+
try {
|
|
44
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
45
|
+
const projectRoot = findProjectRoot(path.dirname(resolvedFilePath));
|
|
46
|
+
const formatter = detectFormatter(projectRoot);
|
|
47
|
+
if (!formatter) return rawInput;
|
|
48
|
+
|
|
49
|
+
const resolved = resolveFormatterBin(projectRoot, formatter);
|
|
50
|
+
if (!resolved) return rawInput;
|
|
51
|
+
|
|
52
|
+
// Biome: `check --write` = format + lint in one pass
|
|
53
|
+
// Prettier: `--write` = format only
|
|
54
|
+
const args = formatter === 'biome' ? [...resolved.prefix, 'check', '--write', resolvedFilePath] : [...resolved.prefix, '--write', resolvedFilePath];
|
|
55
|
+
|
|
56
|
+
if (process.platform === 'win32' && resolved.bin.endsWith('.cmd')) {
|
|
57
|
+
// Windows: .cmd files require shell to execute. Guard against
|
|
58
|
+
// command injection by rejecting paths with shell metacharacters.
|
|
59
|
+
if (UNSAFE_PATH_CHARS.test(resolvedFilePath)) {
|
|
60
|
+
throw new Error('File path contains unsafe shell characters');
|
|
61
|
+
}
|
|
62
|
+
const result = spawnSync(resolved.bin, args, {
|
|
63
|
+
cwd: projectRoot,
|
|
64
|
+
shell: true,
|
|
65
|
+
stdio: 'pipe',
|
|
66
|
+
timeout: 15000
|
|
67
|
+
});
|
|
68
|
+
if (result.error) throw result.error;
|
|
69
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
70
|
+
throw new Error(result.stderr?.toString() || `Formatter exited with status ${result.status}`);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
execFileSync(resolved.bin, args, {
|
|
74
|
+
cwd: projectRoot,
|
|
75
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
76
|
+
timeout: 15000
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Formatter not installed, file missing, or failed — non-blocking
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Invalid input — pass through
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return rawInput;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── stdin entry point (backwards-compatible) ────────────────────
|
|
91
|
+
if (require.main === module) {
|
|
92
|
+
let data = '';
|
|
93
|
+
process.stdin.setEncoding('utf8');
|
|
94
|
+
|
|
95
|
+
process.stdin.on('data', chunk => {
|
|
96
|
+
if (data.length < MAX_STDIN) {
|
|
97
|
+
const remaining = MAX_STDIN - data.length;
|
|
98
|
+
data += chunk.substring(0, remaining);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
process.stdin.on('end', () => {
|
|
103
|
+
data = run(data);
|
|
104
|
+
process.stdout.write(data);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { run };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse Hook: TypeScript check after editing .ts/.tsx files
|
|
4
|
+
*
|
|
5
|
+
* Cross-platform (Windows, macOS, Linux)
|
|
6
|
+
*
|
|
7
|
+
* Runs after Edit tool use on TypeScript files. Walks up from the file's
|
|
8
|
+
* directory to find the nearest tsconfig.json, then runs tsc --noEmit
|
|
9
|
+
* and reports only errors related to the edited file.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { execFileSync } = require("child_process");
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
|
|
16
|
+
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
|
17
|
+
let data = "";
|
|
18
|
+
process.stdin.setEncoding("utf8");
|
|
19
|
+
|
|
20
|
+
process.stdin.on("data", (chunk) => {
|
|
21
|
+
if (data.length < MAX_STDIN) {
|
|
22
|
+
const remaining = MAX_STDIN - data.length;
|
|
23
|
+
data += chunk.substring(0, remaining);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
process.stdin.on("end", () => {
|
|
28
|
+
try {
|
|
29
|
+
const input = JSON.parse(data);
|
|
30
|
+
const filePath = input.tool_input?.file_path;
|
|
31
|
+
|
|
32
|
+
if (filePath && /\.(ts|tsx)$/.test(filePath)) {
|
|
33
|
+
const resolvedPath = path.resolve(filePath);
|
|
34
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
35
|
+
process.stdout.write(data);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
// Find nearest tsconfig.json by walking up (max 20 levels to prevent infinite loop)
|
|
39
|
+
let dir = path.dirname(resolvedPath);
|
|
40
|
+
const root = path.parse(dir).root;
|
|
41
|
+
let depth = 0;
|
|
42
|
+
|
|
43
|
+
while (dir !== root && depth < 20) {
|
|
44
|
+
if (fs.existsSync(path.join(dir, "tsconfig.json"))) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
dir = path.dirname(dir);
|
|
48
|
+
depth++;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (fs.existsSync(path.join(dir, "tsconfig.json"))) {
|
|
52
|
+
try {
|
|
53
|
+
// Use npx.cmd on Windows to avoid shell: true which enables command injection
|
|
54
|
+
const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
55
|
+
execFileSync(npxBin, ["tsc", "--noEmit", "--pretty", "false"], {
|
|
56
|
+
cwd: dir,
|
|
57
|
+
encoding: "utf8",
|
|
58
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
59
|
+
timeout: 30000,
|
|
60
|
+
});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// tsc exits non-zero when there are errors — filter to edited file
|
|
63
|
+
const output = (err.stdout || "") + (err.stderr || "");
|
|
64
|
+
// Compute paths that uniquely identify the edited file.
|
|
65
|
+
// tsc output uses paths relative to its cwd (the tsconfig dir),
|
|
66
|
+
// so check for the relative path, absolute path, and original path.
|
|
67
|
+
// Avoid bare basename matching — it causes false positives when
|
|
68
|
+
// multiple files share the same name (e.g., src/utils.ts vs tests/utils.ts).
|
|
69
|
+
const relPath = path.relative(dir, resolvedPath);
|
|
70
|
+
const candidates = new Set([filePath, resolvedPath, relPath]);
|
|
71
|
+
const relevantLines = output
|
|
72
|
+
.split("\n")
|
|
73
|
+
.filter((line) => {
|
|
74
|
+
for (const candidate of candidates) {
|
|
75
|
+
if (line.includes(candidate)) return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
})
|
|
79
|
+
.slice(0, 10);
|
|
80
|
+
|
|
81
|
+
if (relevantLines.length > 0) {
|
|
82
|
+
console.error(
|
|
83
|
+
"[Hook] TypeScript errors in " + path.basename(filePath) + ":",
|
|
84
|
+
);
|
|
85
|
+
relevantLines.forEach((line) => console.error(line));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Invalid input — pass through
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
process.stdout.write(data);
|
|
95
|
+
process.exit(0);
|
|
96
|
+
});
|