sinapse-ai 7.1.0 → 7.2.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/.sinapse-ai/data/entity-registry.yaml +749 -757
- package/.sinapse-ai/install-manifest.yaml +4 -4
- package/bin/cli.js +116 -75
- package/package.json +2 -1
- package/squads/claude-code-mastery/CHANGELOG.md +22 -0
- package/squads/claude-code-mastery/README.md +146 -0
- package/squads/claude-code-mastery/agents/claude-mastery-chief.md +554 -0
- package/squads/claude-code-mastery/agents/config-engineer.md +865 -0
- package/squads/claude-code-mastery/agents/hooks-architect.md +1013 -0
- package/squads/claude-code-mastery/agents/mcp-integrator.md +791 -0
- package/squads/claude-code-mastery/agents/project-integrator.md +1196 -0
- package/squads/claude-code-mastery/agents/roadmap-sentinel.md +931 -0
- package/squads/claude-code-mastery/agents/skill-craftsman.md +1250 -0
- package/squads/claude-code-mastery/agents/swarm-orqx.md +1008 -0
- package/squads/claude-code-mastery/checklists/agent-team-readiness-checklist.md +88 -0
- package/squads/claude-code-mastery/checklists/brownfield-readiness-checklist.md +91 -0
- package/squads/claude-code-mastery/checklists/change-checklist.md +75 -0
- package/squads/claude-code-mastery/checklists/context-rot-checklist.md +114 -0
- package/squads/claude-code-mastery/checklists/integration-audit-checklist.md +104 -0
- package/squads/claude-code-mastery/checklists/multi-agent-review-checklist.md +77 -0
- package/squads/claude-code-mastery/checklists/pre-push-checklist.md +79 -0
- package/squads/claude-code-mastery/data/ci-cd-patterns.yaml +412 -0
- package/squads/claude-code-mastery/data/claude-code-quick-ref.yaml +314 -0
- package/squads/claude-code-mastery/data/hook-patterns.yaml +512 -0
- package/squads/claude-code-mastery/data/mcp-integration-catalog.yaml +323 -0
- package/squads/claude-code-mastery/data/project-type-signatures.yaml +335 -0
- package/squads/claude-code-mastery/scripts/validate-setup.js +163 -0
- package/squads/claude-code-mastery/squad.yaml +205 -0
- package/squads/claude-code-mastery/tasks/audit-integration.md +219 -0
- package/squads/claude-code-mastery/tasks/audit-settings.md +206 -0
- package/squads/claude-code-mastery/tasks/audit-setup.md +225 -0
- package/squads/claude-code-mastery/tasks/brownfield-setup.md +322 -0
- package/squads/claude-code-mastery/tasks/ci-cd-setup.md +335 -0
- package/squads/claude-code-mastery/tasks/claude-md-engineer.md +334 -0
- package/squads/claude-code-mastery/tasks/configure-claude-code.md +215 -0
- package/squads/claude-code-mastery/tasks/context-rot-audit.md +329 -0
- package/squads/claude-code-mastery/tasks/create-agent-definition.md +278 -0
- package/squads/claude-code-mastery/tasks/create-rules.md +206 -0
- package/squads/claude-code-mastery/tasks/create-team-topology.md +258 -0
- package/squads/claude-code-mastery/tasks/diagnose.md +166 -0
- package/squads/claude-code-mastery/tasks/enterprise-config.md +346 -0
- package/squads/claude-code-mastery/tasks/hook-designer.md +272 -0
- package/squads/claude-code-mastery/tasks/integrate-project.md +304 -0
- package/squads/claude-code-mastery/tasks/mcp-integration-plan.md +229 -0
- package/squads/claude-code-mastery/tasks/mcp-workflow.md +285 -0
- package/squads/claude-code-mastery/tasks/multi-project-setup.md +228 -0
- package/squads/claude-code-mastery/tasks/optimize-context.md +217 -0
- package/squads/claude-code-mastery/tasks/optimize-workflow.md +226 -0
- package/squads/claude-code-mastery/tasks/parallel-decomposition.md +293 -0
- package/squads/claude-code-mastery/tasks/permission-strategy.md +266 -0
- package/squads/claude-code-mastery/tasks/sandbox-setup.md +279 -0
- package/squads/claude-code-mastery/tasks/setup-repository.md +230 -0
- package/squads/claude-code-mastery/tasks/setup-wizard.md +236 -0
- package/squads/claude-code-mastery/tasks/worktree-strategy.md +320 -0
- package/squads/claude-code-mastery/templates/claude-md-fullstack.md +147 -0
- package/squads/claude-code-mastery/templates/claude-md-library.md +175 -0
- package/squads/claude-code-mastery/templates/claude-md-microservices.md +186 -0
- package/squads/claude-code-mastery/templates/claude-md-mobile.md +198 -0
- package/squads/claude-code-mastery/templates/claude-md-monorepo.md +139 -0
- package/squads/claude-code-mastery/templates/github-actions-claude-ci.yml +348 -0
- package/squads/claude-code-mastery/templates/github-actions-claude-review.yml +179 -0
- package/squads/claude-code-mastery/workflows/wf-audit-complete.yaml +140 -0
- package/squads/claude-code-mastery/workflows/wf-knowledge-update.yaml +165 -0
- package/squads/claude-code-mastery/workflows/wf-project-setup.yaml +192 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# Hook Patterns — Common Automation Recipes for Claude Code
|
|
2
|
+
# Squad: claude-code-mastery
|
|
3
|
+
# Last updated: 2026-03-02
|
|
4
|
+
|
|
5
|
+
version: "1.0.0"
|
|
6
|
+
|
|
7
|
+
# Each pattern provides a complete, copy-paste-ready hook configuration
|
|
8
|
+
# with explanation and settings.json integration.
|
|
9
|
+
|
|
10
|
+
patterns:
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# 1. DAMAGE CONTROL — Block destructive commands
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
- name: damage-control
|
|
16
|
+
event: PreToolUse
|
|
17
|
+
description: |
|
|
18
|
+
Intercept Bash tool calls and block dangerous commands before execution.
|
|
19
|
+
Prevents accidental rm -rf, git push --force, DROP TABLE, etc.
|
|
20
|
+
matcher: "Bash"
|
|
21
|
+
severity: critical
|
|
22
|
+
settings_json_snippet: |
|
|
23
|
+
{
|
|
24
|
+
"hooks": {
|
|
25
|
+
"PreToolUse": [
|
|
26
|
+
{
|
|
27
|
+
"matcher": "Bash",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "python3 ~/.claude/hooks/damage-control.py"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
code_example:
|
|
39
|
+
language: python
|
|
40
|
+
filename: "~/.claude/hooks/damage-control.py"
|
|
41
|
+
code: |
|
|
42
|
+
#!/usr/bin/env python3
|
|
43
|
+
"""Damage control hook — block destructive Bash commands."""
|
|
44
|
+
import json
|
|
45
|
+
import sys
|
|
46
|
+
import re
|
|
47
|
+
|
|
48
|
+
BLOCKED_PATTERNS = [
|
|
49
|
+
r'\brm\s+(-[rRf]+\s+|--recursive)', # rm -rf
|
|
50
|
+
r'\bgit\s+push\s+--force', # git push --force
|
|
51
|
+
r'\bgit\s+reset\s+--hard', # git reset --hard
|
|
52
|
+
r'\bgit\s+clean\s+-[fd]', # git clean -f/-d
|
|
53
|
+
r'\bDROP\s+(TABLE|DATABASE|SCHEMA)', # SQL destructive
|
|
54
|
+
r'\bTRUNCATE\s+TABLE', # SQL truncate
|
|
55
|
+
r'\bchmod\s+777', # Insecure perms
|
|
56
|
+
r'\bcurl\b.*\|\s*(sudo\s+)?bash', # Pipe curl to bash
|
|
57
|
+
r'\bsudo\s+rm\b', # sudo rm
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
def main():
|
|
61
|
+
input_data = json.loads(sys.stdin.read())
|
|
62
|
+
tool_input = input_data.get("tool_input", {})
|
|
63
|
+
command = tool_input.get("command", "")
|
|
64
|
+
|
|
65
|
+
for pattern in BLOCKED_PATTERNS:
|
|
66
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
67
|
+
result = {
|
|
68
|
+
"decision": "block",
|
|
69
|
+
"reason": f"Blocked by damage-control: matches '{pattern}'"
|
|
70
|
+
}
|
|
71
|
+
print(json.dumps(result))
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
print(json.dumps({"decision": "approve"}))
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# 2. AUTO-LINT — Run linter after file modifications
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
- name: auto-lint
|
|
83
|
+
event: PostToolUse
|
|
84
|
+
description: |
|
|
85
|
+
Automatically run linter/formatter after Write or Edit tool calls.
|
|
86
|
+
Catches style issues immediately, maintaining code quality.
|
|
87
|
+
matcher: "Write|Edit"
|
|
88
|
+
severity: low
|
|
89
|
+
settings_json_snippet: |
|
|
90
|
+
{
|
|
91
|
+
"hooks": {
|
|
92
|
+
"PostToolUse": [
|
|
93
|
+
{
|
|
94
|
+
"matcher": "Write|Edit",
|
|
95
|
+
"hooks": [
|
|
96
|
+
{
|
|
97
|
+
"type": "command",
|
|
98
|
+
"command": "python3 ~/.claude/hooks/auto-lint.py"
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
code_example:
|
|
106
|
+
language: python
|
|
107
|
+
filename: "~/.claude/hooks/auto-lint.py"
|
|
108
|
+
code: |
|
|
109
|
+
#!/usr/bin/env python3
|
|
110
|
+
"""Auto-lint hook — run appropriate linter after file changes."""
|
|
111
|
+
import json
|
|
112
|
+
import subprocess
|
|
113
|
+
import sys
|
|
114
|
+
import os
|
|
115
|
+
|
|
116
|
+
LINTERS = {
|
|
117
|
+
".ts": "npx eslint --fix {file}",
|
|
118
|
+
".tsx": "npx eslint --fix {file}",
|
|
119
|
+
".js": "npx eslint --fix {file}",
|
|
120
|
+
".jsx": "npx eslint --fix {file}",
|
|
121
|
+
".py": "ruff check --fix {file}",
|
|
122
|
+
".rs": "rustfmt {file}",
|
|
123
|
+
".go": "gofmt -w {file}",
|
|
124
|
+
".css": "npx prettier --write {file}",
|
|
125
|
+
".json": "npx prettier --write {file}",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
def main():
|
|
129
|
+
input_data = json.loads(sys.stdin.read())
|
|
130
|
+
tool_input = input_data.get("tool_input", {})
|
|
131
|
+
file_path = tool_input.get("file_path", "")
|
|
132
|
+
|
|
133
|
+
if not file_path or not os.path.exists(file_path):
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
_, ext = os.path.splitext(file_path)
|
|
137
|
+
lint_cmd = LINTERS.get(ext)
|
|
138
|
+
|
|
139
|
+
if lint_cmd:
|
|
140
|
+
cmd = lint_cmd.format(file=file_path)
|
|
141
|
+
try:
|
|
142
|
+
subprocess.run(
|
|
143
|
+
cmd, shell=True, capture_output=True,
|
|
144
|
+
timeout=30, cwd=os.getcwd()
|
|
145
|
+
)
|
|
146
|
+
except (subprocess.TimeoutExpired, Exception):
|
|
147
|
+
pass # Non-blocking — lint failure should not stop work
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
main()
|
|
151
|
+
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
# 3. NOTIFICATION — Send alerts to Slack/Discord
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
- name: notification
|
|
156
|
+
event: Notification
|
|
157
|
+
description: |
|
|
158
|
+
Forward Claude Code notifications to external channels.
|
|
159
|
+
Useful for team awareness of agent activity.
|
|
160
|
+
matcher: ""
|
|
161
|
+
severity: low
|
|
162
|
+
settings_json_snippet: |
|
|
163
|
+
{
|
|
164
|
+
"hooks": {
|
|
165
|
+
"Notification": [
|
|
166
|
+
{
|
|
167
|
+
"matcher": "",
|
|
168
|
+
"hooks": [
|
|
169
|
+
{
|
|
170
|
+
"type": "command",
|
|
171
|
+
"command": "python3 ~/.claude/hooks/notify.py"
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
code_example:
|
|
179
|
+
language: python
|
|
180
|
+
filename: "~/.claude/hooks/notify.py"
|
|
181
|
+
code: |
|
|
182
|
+
#!/usr/bin/env python3
|
|
183
|
+
"""Notification hook — forward to Slack webhook."""
|
|
184
|
+
import json
|
|
185
|
+
import sys
|
|
186
|
+
import urllib.request
|
|
187
|
+
|
|
188
|
+
SLACK_WEBHOOK = os.environ.get("SLACK_WEBHOOK_URL", "")
|
|
189
|
+
|
|
190
|
+
def main():
|
|
191
|
+
if not SLACK_WEBHOOK:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
input_data = json.loads(sys.stdin.read())
|
|
195
|
+
message = input_data.get("message", "Claude Code notification")
|
|
196
|
+
title = input_data.get("title", "Notification")
|
|
197
|
+
|
|
198
|
+
payload = {
|
|
199
|
+
"text": f"*{title}*\n{message}",
|
|
200
|
+
"username": "Claude Code",
|
|
201
|
+
"icon_emoji": ":robot_face:"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
req = urllib.request.Request(
|
|
205
|
+
SLACK_WEBHOOK,
|
|
206
|
+
data=json.dumps(payload).encode(),
|
|
207
|
+
headers={"Content-Type": "application/json"}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
urllib.request.urlopen(req, timeout=5)
|
|
212
|
+
except Exception:
|
|
213
|
+
pass # Non-blocking
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
import os
|
|
217
|
+
main()
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# 4. CONTEXT PRESERVATION — Save state before compaction
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
- name: context-preservation
|
|
223
|
+
event: PreCompact
|
|
224
|
+
description: |
|
|
225
|
+
Before context window compaction, save critical state to disk.
|
|
226
|
+
Preserves important decisions, file lists, and progress that
|
|
227
|
+
would otherwise be lost during compaction.
|
|
228
|
+
matcher: ""
|
|
229
|
+
severity: medium
|
|
230
|
+
settings_json_snippet: |
|
|
231
|
+
{
|
|
232
|
+
"hooks": {
|
|
233
|
+
"PreCompact": [
|
|
234
|
+
{
|
|
235
|
+
"matcher": "",
|
|
236
|
+
"hooks": [
|
|
237
|
+
{
|
|
238
|
+
"type": "command",
|
|
239
|
+
"command": "python3 ~/.claude/hooks/preserve-context.py"
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
code_example:
|
|
247
|
+
language: python
|
|
248
|
+
filename: "~/.claude/hooks/preserve-context.py"
|
|
249
|
+
code: |
|
|
250
|
+
#!/usr/bin/env python3
|
|
251
|
+
"""PreCompact hook — preserve critical context before compaction."""
|
|
252
|
+
import json
|
|
253
|
+
import sys
|
|
254
|
+
import os
|
|
255
|
+
from datetime import datetime
|
|
256
|
+
|
|
257
|
+
PRESERVE_DIR = os.path.join(os.getcwd(), ".claude", "preserved-context")
|
|
258
|
+
|
|
259
|
+
def main():
|
|
260
|
+
input_data = json.loads(sys.stdin.read())
|
|
261
|
+
session_id = input_data.get("session_id", "unknown")
|
|
262
|
+
conversation = input_data.get("conversation_summary", "")
|
|
263
|
+
|
|
264
|
+
os.makedirs(PRESERVE_DIR, exist_ok=True)
|
|
265
|
+
|
|
266
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
267
|
+
filename = f"context-{session_id[:8]}-{timestamp}.json"
|
|
268
|
+
filepath = os.path.join(PRESERVE_DIR, filename)
|
|
269
|
+
|
|
270
|
+
preserved = {
|
|
271
|
+
"timestamp": timestamp,
|
|
272
|
+
"session_id": session_id,
|
|
273
|
+
"summary": conversation,
|
|
274
|
+
"cwd": os.getcwd(),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
with open(filepath, "w") as f:
|
|
278
|
+
json.dump(preserved, f, indent=2)
|
|
279
|
+
|
|
280
|
+
if __name__ == "__main__":
|
|
281
|
+
main()
|
|
282
|
+
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
# 5. COST TRACKING — Log token usage on session end
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
- name: cost-tracking
|
|
287
|
+
event: Stop
|
|
288
|
+
description: |
|
|
289
|
+
Track token usage and estimated cost at the end of each agent turn.
|
|
290
|
+
Writes to a JSONL log for analysis and budgeting.
|
|
291
|
+
matcher: ""
|
|
292
|
+
severity: low
|
|
293
|
+
settings_json_snippet: |
|
|
294
|
+
{
|
|
295
|
+
"hooks": {
|
|
296
|
+
"Stop": [
|
|
297
|
+
{
|
|
298
|
+
"matcher": "",
|
|
299
|
+
"hooks": [
|
|
300
|
+
{
|
|
301
|
+
"type": "command",
|
|
302
|
+
"command": "python3 ~/.claude/hooks/cost-tracker.py"
|
|
303
|
+
}
|
|
304
|
+
]
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
code_example:
|
|
310
|
+
language: python
|
|
311
|
+
filename: "~/.claude/hooks/cost-tracker.py"
|
|
312
|
+
code: |
|
|
313
|
+
#!/usr/bin/env python3
|
|
314
|
+
"""Stop hook — log token usage and estimated cost."""
|
|
315
|
+
import json
|
|
316
|
+
import sys
|
|
317
|
+
import os
|
|
318
|
+
from datetime import datetime
|
|
319
|
+
|
|
320
|
+
LOG_FILE = os.path.expanduser("~/.claude/logs/cost-tracking.jsonl")
|
|
321
|
+
|
|
322
|
+
# Approximate pricing per 1M tokens (Claude Sonnet 4)
|
|
323
|
+
INPUT_COST_PER_M = 3.0
|
|
324
|
+
OUTPUT_COST_PER_M = 15.0
|
|
325
|
+
|
|
326
|
+
def main():
|
|
327
|
+
input_data = json.loads(sys.stdin.read())
|
|
328
|
+
|
|
329
|
+
input_tokens = input_data.get("input_tokens", 0)
|
|
330
|
+
output_tokens = input_data.get("output_tokens", 0)
|
|
331
|
+
session_id = input_data.get("session_id", "unknown")
|
|
332
|
+
|
|
333
|
+
cost_input = (input_tokens / 1_000_000) * INPUT_COST_PER_M
|
|
334
|
+
cost_output = (output_tokens / 1_000_000) * OUTPUT_COST_PER_M
|
|
335
|
+
total_cost = cost_input + cost_output
|
|
336
|
+
|
|
337
|
+
entry = {
|
|
338
|
+
"timestamp": datetime.now().isoformat(),
|
|
339
|
+
"session_id": session_id,
|
|
340
|
+
"input_tokens": input_tokens,
|
|
341
|
+
"output_tokens": output_tokens,
|
|
342
|
+
"estimated_cost_usd": round(total_cost, 4),
|
|
343
|
+
"project": os.path.basename(os.getcwd()),
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
|
|
347
|
+
with open(LOG_FILE, "a") as f:
|
|
348
|
+
f.write(json.dumps(entry) + "\n")
|
|
349
|
+
|
|
350
|
+
if __name__ == "__main__":
|
|
351
|
+
main()
|
|
352
|
+
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
# 6. SECURITY GATE — Validate no secrets exposure
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
- name: security-gate
|
|
357
|
+
event: PreToolUse
|
|
358
|
+
description: |
|
|
359
|
+
Scan tool inputs for secrets, API keys, and credentials before execution.
|
|
360
|
+
Blocks Write/Edit calls that would commit sensitive data.
|
|
361
|
+
matcher: "Write|Edit|Bash"
|
|
362
|
+
severity: critical
|
|
363
|
+
settings_json_snippet: |
|
|
364
|
+
{
|
|
365
|
+
"hooks": {
|
|
366
|
+
"PreToolUse": [
|
|
367
|
+
{
|
|
368
|
+
"matcher": "Write|Edit|Bash",
|
|
369
|
+
"hooks": [
|
|
370
|
+
{
|
|
371
|
+
"type": "command",
|
|
372
|
+
"command": "python3 ~/.claude/hooks/security-gate.py"
|
|
373
|
+
}
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
]
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
code_example:
|
|
380
|
+
language: python
|
|
381
|
+
filename: "~/.claude/hooks/security-gate.py"
|
|
382
|
+
code: |
|
|
383
|
+
#!/usr/bin/env python3
|
|
384
|
+
"""Security gate — detect secrets in tool inputs."""
|
|
385
|
+
import json
|
|
386
|
+
import sys
|
|
387
|
+
import re
|
|
388
|
+
|
|
389
|
+
SECRET_PATTERNS = [
|
|
390
|
+
(r'(?:api[_-]?key|apikey)\s*[:=]\s*["\']?[a-zA-Z0-9_\-]{20,}', "API Key"),
|
|
391
|
+
(r'(?:secret|password|passwd|pwd)\s*[:=]\s*["\']?[^\s"\']{8,}', "Password/Secret"),
|
|
392
|
+
(r'(?:token)\s*[:=]\s*["\']?[a-zA-Z0-9_\-\.]{20,}', "Token"),
|
|
393
|
+
(r'sk-[a-zA-Z0-9]{32,}', "OpenAI API Key"),
|
|
394
|
+
(r'ghp_[a-zA-Z0-9]{36}', "GitHub Personal Access Token"),
|
|
395
|
+
(r'-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', "Private Key"),
|
|
396
|
+
(r'AKIA[0-9A-Z]{16}', "AWS Access Key"),
|
|
397
|
+
(r'mongodb\+srv://[^\s]+', "MongoDB Connection String"),
|
|
398
|
+
(r'postgres(?:ql)?://[^\s]+@[^\s]+', "PostgreSQL Connection String"),
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
SAFE_FILES = [".env.example", ".env.template", ".env.sample"]
|
|
402
|
+
|
|
403
|
+
def main():
|
|
404
|
+
input_data = json.loads(sys.stdin.read())
|
|
405
|
+
tool_name = input_data.get("tool_name", "")
|
|
406
|
+
tool_input = input_data.get("tool_input", {})
|
|
407
|
+
|
|
408
|
+
content = ""
|
|
409
|
+
file_path = tool_input.get("file_path", "")
|
|
410
|
+
|
|
411
|
+
if tool_name in ("Write", "Edit"):
|
|
412
|
+
content = tool_input.get("content", "") + tool_input.get("new_string", "")
|
|
413
|
+
elif tool_name == "Bash":
|
|
414
|
+
content = tool_input.get("command", "")
|
|
415
|
+
|
|
416
|
+
# Skip safe files
|
|
417
|
+
if any(file_path.endswith(sf) for sf in SAFE_FILES):
|
|
418
|
+
print(json.dumps({"decision": "approve"}))
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
for pattern, label in SECRET_PATTERNS:
|
|
422
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
423
|
+
result = {
|
|
424
|
+
"decision": "block",
|
|
425
|
+
"reason": f"Security gate: potential {label} detected in {tool_name} input"
|
|
426
|
+
}
|
|
427
|
+
print(json.dumps(result))
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
print(json.dumps({"decision": "approve"}))
|
|
431
|
+
|
|
432
|
+
if __name__ == "__main__":
|
|
433
|
+
main()
|
|
434
|
+
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
# 7. TIMING LOGGER — Performance instrumentation
|
|
437
|
+
# ---------------------------------------------------------------------------
|
|
438
|
+
- name: timing-logger
|
|
439
|
+
event: PreToolUse
|
|
440
|
+
paired_event: PostToolUse
|
|
441
|
+
description: |
|
|
442
|
+
Log timestamps for every tool call to enable performance analysis.
|
|
443
|
+
Paired PreToolUse/PostToolUse hooks create a complete timeline.
|
|
444
|
+
matcher: ""
|
|
445
|
+
severity: low
|
|
446
|
+
settings_json_snippet: |
|
|
447
|
+
{
|
|
448
|
+
"hooks": {
|
|
449
|
+
"PreToolUse": [
|
|
450
|
+
{
|
|
451
|
+
"matcher": "",
|
|
452
|
+
"hooks": [
|
|
453
|
+
{
|
|
454
|
+
"type": "command",
|
|
455
|
+
"command": "node ~/.claude/hooks/timing-logger.js pre"
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
}
|
|
459
|
+
],
|
|
460
|
+
"PostToolUse": [
|
|
461
|
+
{
|
|
462
|
+
"matcher": "",
|
|
463
|
+
"hooks": [
|
|
464
|
+
{
|
|
465
|
+
"type": "command",
|
|
466
|
+
"command": "node ~/.claude/hooks/timing-logger.js post"
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
}
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
code_example:
|
|
474
|
+
language: javascript
|
|
475
|
+
filename: "~/.claude/hooks/timing-logger.js"
|
|
476
|
+
code: |
|
|
477
|
+
#!/usr/bin/env node
|
|
478
|
+
/**
|
|
479
|
+
* Timing logger hook - records PreToolUse/PostToolUse timestamps.
|
|
480
|
+
* Usage: node timing-logger.js pre|post
|
|
481
|
+
* Reads tool data from stdin, writes JSONL to ~/.claude/logs/timing-YYYY-MM-DD.jsonl
|
|
482
|
+
*/
|
|
483
|
+
const fs = require('fs');
|
|
484
|
+
const path = require('path');
|
|
485
|
+
const os = require('os');
|
|
486
|
+
|
|
487
|
+
const phase = process.argv[2]; // 'pre' or 'post'
|
|
488
|
+
const LOG_DIR = path.join(os.homedir(), '.claude', 'logs');
|
|
489
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
490
|
+
const logFile = path.join(LOG_DIR, `timing-${today}.jsonl`);
|
|
491
|
+
|
|
492
|
+
let input = '';
|
|
493
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
494
|
+
process.stdin.on('end', () => {
|
|
495
|
+
try {
|
|
496
|
+
const data = JSON.parse(input);
|
|
497
|
+
const entry = {
|
|
498
|
+
timestamp: new Date().toISOString(),
|
|
499
|
+
epochMs: Date.now(),
|
|
500
|
+
event: phase === 'pre' ? 'PreToolUse' : 'PostToolUse',
|
|
501
|
+
tool: data.tool_name || 'unknown',
|
|
502
|
+
session: data.session_id || 'unknown',
|
|
503
|
+
};
|
|
504
|
+
if (phase === 'post' && data.duration_ms) {
|
|
505
|
+
entry.durationMs = data.duration_ms;
|
|
506
|
+
}
|
|
507
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
508
|
+
fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
|
|
509
|
+
} catch (e) {
|
|
510
|
+
// Non-blocking — do not fail the tool call
|
|
511
|
+
}
|
|
512
|
+
});
|