kodelyth-ecc 1.5.9 → 1.7.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.
Files changed (123) hide show
  1. package/.github/workflows/publish.yml +19 -0
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +455 -0
  4. package/CLAUDE.md +48 -16
  5. package/README.md +265 -81
  6. package/VERSION +1 -1
  7. package/actions/ecc-review/README.md +243 -0
  8. package/actions/ecc-review/action.yml +158 -0
  9. package/actions/ecc-review/post-comment.js +171 -0
  10. package/actions/ecc-review/run-review.js +285 -0
  11. package/agents/backdoor-hunter.md +260 -0
  12. package/agents/chaos-engineer.md +251 -0
  13. package/agents/code-stealer-detector.md +228 -0
  14. package/agents/jailbreak-tester.md +222 -0
  15. package/agents/license-violation-finder.md +212 -0
  16. package/agents/prompt-injection-hunter.md +126 -0
  17. package/agents/secret-hunter.md +209 -0
  18. package/agents/supply-chain-auditor.md +195 -0
  19. package/bin/kodelyth-ecc.js +875 -1
  20. package/bundles/enterprise.md +172 -0
  21. package/bundles/indie-hacker.md +106 -0
  22. package/bundles/red-team.md +138 -0
  23. package/cat +0 -0
  24. package/commands/dashboard.md +67 -0
  25. package/commands/devil-mode.md +121 -0
  26. package/commands/memory-evolve.md +71 -0
  27. package/commands/replay.md +61 -0
  28. package/commands/route-model.md +48 -0
  29. package/commands/swarm.md +68 -0
  30. package/commands/verify-supply-chain.md +59 -0
  31. package/docs/dashboard.md +211 -0
  32. package/docs/evolve.md +303 -0
  33. package/docs/mcp-clients.md +167 -0
  34. package/docs/mcp.md +178 -0
  35. package/docs/replay.md +244 -0
  36. package/docs/supply-chain.md +207 -0
  37. package/docs/swarm.md +243 -0
  38. package/hooks/hooks.json +52 -0
  39. package/hooks/memory/auto-recall.js +29 -1
  40. package/hooks/safety/README.md +124 -0
  41. package/hooks/safety/lib/patterns.js +179 -0
  42. package/hooks/safety/prompt-injection-guard.js +179 -0
  43. package/hooks/safety/token-budget.js +229 -0
  44. package/install.ps1 +199 -2
  45. package/install.sh +286 -4
  46. package/package.json +30 -4
  47. package/rules/common/agent-intent-routing.md +182 -0
  48. package/rules/common/cost-aware-model-routing.md +152 -0
  49. package/scripts/dashboard/data.js +353 -0
  50. package/scripts/dashboard/server.js +324 -0
  51. package/scripts/dashboard/static/index.html +645 -0
  52. package/scripts/evolve/analyze.js +303 -0
  53. package/scripts/evolve/proposals.js +162 -0
  54. package/scripts/evolve/stats.js +219 -0
  55. package/scripts/mcp/catalog.js +244 -0
  56. package/scripts/mcp/client.js +181 -0
  57. package/scripts/mcp/prompts.js +133 -0
  58. package/scripts/mcp/resources.js +94 -0
  59. package/scripts/mcp/server.js +153 -0
  60. package/scripts/mcp/tools.js +465 -0
  61. package/scripts/replay/bundle.js +191 -0
  62. package/scripts/replay/replay.js +107 -0
  63. package/scripts/router/classify.js +232 -0
  64. package/scripts/supply-chain/manifest.js +154 -0
  65. package/scripts/supply-chain/sbom.js +202 -0
  66. package/scripts/supply-chain/verify.js +102 -0
  67. package/scripts/swarm/build-plan.js +193 -0
  68. package/skills/cost-aware-model-routing/SKILL.md +153 -0
  69. package/skills/kodelyth-quickstart/SKILL.md +7 -0
  70. package/skills/observability-dashboard/SKILL.md +119 -0
  71. package/skills/self-evolving-memory/SKILL.md +175 -0
  72. package/skills/session-replay/SKILL.md +199 -0
  73. package/skills/supply-chain-verification/SKILL.md +201 -0
  74. package/skills/swarm-orchestrator/SKILL.md +177 -0
  75. package/social/card-install.svg +1 -1
  76. package/social/facebook-group/POST.md +121 -0
  77. package/social/facebook-group/fb-1-3am-debug.png +0 -0
  78. package/social/facebook-group/fb-1-3am-debug.svg +97 -0
  79. package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
  80. package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
  81. package/social/facebook-group/fb-3-before-after.png +0 -0
  82. package/social/facebook-group/fb-3-before-after.svg +94 -0
  83. package/social/facebook-v150.svg +5 -5
  84. package/social/github-social-preview.svg +119 -100
  85. package/social/readme-hero.svg +11 -11
  86. package/social/section-agents.svg +57 -0
  87. package/social/section-author.svg +54 -0
  88. package/social/section-dashboard.svg +59 -0
  89. package/social/section-devil.svg +54 -0
  90. package/social/section-hooks.svg +51 -0
  91. package/social/section-install.svg +42 -0
  92. package/social/section-learning.svg +52 -0
  93. package/social/section-mcp.svg +46 -0
  94. package/social/section-memory.svg +57 -0
  95. package/social/section-parallel.svg +72 -0
  96. package/social/section-routing.svg +51 -0
  97. package/social/x-card-agents-grid.svg +6 -6
  98. package/tests/dashboard/data.test.js +235 -0
  99. package/tests/dashboard/server.test.js +240 -0
  100. package/tests/evolve/analyze.test.js +169 -0
  101. package/tests/evolve/proposals.test.js +173 -0
  102. package/tests/evolve/stats.test.js +159 -0
  103. package/tests/mcp/catalog.test.js +98 -0
  104. package/tests/mcp/client.test.js +109 -0
  105. package/tests/mcp/resources-prompts.test.js +70 -0
  106. package/tests/mcp/tools.test.js +159 -0
  107. package/tests/replay/bundle.test.js +181 -0
  108. package/tests/replay/replay.test.js +131 -0
  109. package/tests/router/classify.test.js +164 -0
  110. package/tests/safety/patterns.test.js +94 -0
  111. package/tests/safety/prompt-injection-guard.test.js +111 -0
  112. package/tests/safety/token-budget.test.js +119 -0
  113. package/tests/supply-chain/manifest.test.js +147 -0
  114. package/tests/supply-chain/sbom.test.js +170 -0
  115. package/tests/supply-chain/verify.test.js +146 -0
  116. package/tests/swarm/build-plan.test.js +188 -0
  117. package/wiki/Agent-Reference.md +58 -7
  118. package/wiki/FAQ.md +204 -7
  119. package/wiki/Home.md +104 -28
  120. package/wiki/Hook-Reference.md +1 -1
  121. package/wiki/Installation-Guide.md +109 -6
  122. package/wiki/Platform-Support.md +239 -25
  123. package/wiki/Skill-Reference.md +101 -6
package/hooks/hooks.json CHANGED
@@ -152,6 +152,19 @@
152
152
  ],
153
153
  "description": "Kodelyth Memory: auto-detect topic from each user prompt and inject relevant past memories before the AI responds (suppresses repeats per session)",
154
154
  "id": "kodelyth:user-prompt:memory-auto-recall"
155
+ },
156
+ {
157
+ "matcher": "*",
158
+ "hooks": [
159
+ {
160
+ "type": "command",
161
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/safety/prompt-injection-guard.js\"",
162
+ "async": false,
163
+ "timeout": 3
164
+ }
165
+ ],
166
+ "description": "Kodelyth Safety: scan user prompts for jailbreak / instruction-override patterns. Opt-in via KODELYTH_PI_GUARD=warn|block (default off).",
167
+ "id": "kodelyth:user-prompt:prompt-injection-guard"
155
168
  }
156
169
  ],
157
170
  "PreCompact": [
@@ -168,6 +181,19 @@
168
181
  }
169
182
  ],
170
183
  "SessionStart": [
184
+ {
185
+ "matcher": "*",
186
+ "hooks": [
187
+ {
188
+ "type": "command",
189
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/safety/token-budget.js\"",
190
+ "async": false,
191
+ "timeout": 3
192
+ }
193
+ ],
194
+ "description": "Kodelyth Safety: enforce per-session token budget. Opt-in via KODELYTH_TOKEN_BUDGET=<tokens>|warn (default off).",
195
+ "id": "kodelyth:session:start:token-budget"
196
+ },
171
197
  {
172
198
  "matcher": "*",
173
199
  "hooks": [
@@ -337,6 +363,19 @@
337
363
  ],
338
364
  "description": "Capture tool use results for continuous learning",
339
365
  "id": "post:observe:continuous-learning"
366
+ },
367
+ {
368
+ "matcher": "Read|WebFetch|mcp__.*",
369
+ "hooks": [
370
+ {
371
+ "type": "command",
372
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/safety/prompt-injection-guard.js\"",
373
+ "async": false,
374
+ "timeout": 3
375
+ }
376
+ ],
377
+ "description": "Kodelyth Safety: scan Read/WebFetch/MCP tool responses for indirect prompt-injection. Opt-in via KODELYTH_PI_GUARD=warn|block (default off).",
378
+ "id": "kodelyth:post-tool:prompt-injection-guard"
340
379
  }
341
380
  ],
342
381
  "PostToolUseFailure": [
@@ -353,6 +392,19 @@
353
392
  }
354
393
  ],
355
394
  "Stop": [
395
+ {
396
+ "matcher": "*",
397
+ "hooks": [
398
+ {
399
+ "type": "command",
400
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/safety/token-budget.js\"",
401
+ "async": false,
402
+ "timeout": 3
403
+ }
404
+ ],
405
+ "description": "Kodelyth Safety: accumulate per-session token usage and warn/block on budget threshold. Opt-in via KODELYTH_TOKEN_BUDGET=<tokens>|warn (default off).",
406
+ "id": "kodelyth:stop:token-budget"
407
+ },
356
408
  {
357
409
  "matcher": "*",
358
410
  "hooks": [
@@ -50,7 +50,13 @@ function main() {
50
50
  limit: MAX_RECALLED * 2, // grab extra so we can filter out repeats
51
51
  minScore: MIN_SCORE,
52
52
  });
53
- if (matches.length === 0) return done({});
53
+
54
+ // Phase 3.4 — record signal for self-evolving memory.
55
+ // Fire-and-forget; never block recall path on stats failure.
56
+ if (matches.length === 0) {
57
+ tryRecordRoutingMiss({ prompt: userPrompt, sessionId, projectRoot });
58
+ return done({});
59
+ }
54
60
 
55
61
  // Filter out memories already surfaced this session
56
62
  const surfacedFile = surfacedStatePath(sessionId);
@@ -62,6 +68,11 @@ function main() {
62
68
  for (const m of fresh) surfaced.add(m.id);
63
69
  saveSurfaced(surfacedFile, surfaced);
64
70
 
71
+ // Phase 3.4 — bump reuse counters for each freshly surfaced memory.
72
+ for (const m of fresh) {
73
+ tryRecordSurface({ memoryId: m.id, sessionId, projectRoot });
74
+ }
75
+
65
76
  const block = formatBlock(fresh, userPrompt);
66
77
  done({
67
78
  additionalContext: block,
@@ -131,6 +142,23 @@ function saveSurfaced(file, set) {
131
142
  }
132
143
  }
133
144
 
145
+ // ── Phase 3.4 — self-evolving memory signals ────────────────────────────────
146
+ // Lazy-require the evolve stats module so the hook does not pay the cost
147
+ // (or crash on a broken module) unless we actually have something to record.
148
+ function tryRecordSurface(args) {
149
+ try {
150
+ const stats = require(path.join(__dirname, '..', '..', 'scripts', 'evolve', 'stats.js'));
151
+ stats.recordSurface(args);
152
+ } catch { /* never break the hook */ }
153
+ }
154
+
155
+ function tryRecordRoutingMiss(args) {
156
+ try {
157
+ const stats = require(path.join(__dirname, '..', '..', 'scripts', 'evolve', 'stats.js'));
158
+ stats.recordRoutingMiss(args);
159
+ } catch { /* never break the hook */ }
160
+ }
161
+
134
162
  function safeJson(s) {
135
163
  try { return JSON.parse(s); } catch { return {}; }
136
164
  }
@@ -0,0 +1,124 @@
1
+ # Kodelyth ECC — Safety Hooks
2
+
3
+ Two production-ready safety primitives that ride on the existing ECC hook pipeline. Both are **off by default** — explicitly opt in via env vars so existing users see zero behavior change unless they ask for it.
4
+
5
+ > Phase 2.10 of the [Devil Roadmap](../../README.md). Local-only, deterministic regex-based detection. No LLM calls. No telemetry.
6
+
7
+ ---
8
+
9
+ ## 1. `prompt-injection-guard.js`
10
+
11
+ Scans inbound text for jailbreak / instruction-override patterns **before the model sees it**. Wires into:
12
+
13
+ - `UserPromptSubmit` — scans the user's outbound message
14
+ - `PostToolUse` on `Read|WebFetch|mcp__*` — scans tool responses for indirect injection from external content
15
+
16
+ ### Modes
17
+
18
+ | `KODELYTH_PI_GUARD` | Behavior |
19
+ |---|---|
20
+ | `off` (or unset) | Hook does nothing. Default. |
21
+ | `warn` | Always exits 0. Prints findings to stderr (visible in transcript). |
22
+ | `block` | Exits 2 (block) on **critical** findings. Warns on high. Passes on medium. |
23
+
24
+ ### Severity tiers
25
+
26
+ - **critical** — instruction override, system-prompt extraction, role rebind (DAN/dev-mode), exfiltration channel, decoded base64 jailbreak payload
27
+ - **high** — hidden system markers (`[[SYSTEM]]`, `<|system|>`, `### NEW INSTRUCTIONS`), tool-call hijack patterns, jailbreak canaries, demands for unrestricted output
28
+ - **medium** — generic "new instructions" keywords, invisible/bidi Unicode, large base64 blobs
29
+
30
+ The full pattern catalog lives in `lib/patterns.js` — auditable and contributor-friendly.
31
+
32
+ ### Optional knobs
33
+
34
+ ```bash
35
+ KODELYTH_PI_GUARD_LOG=/path/to/findings.jsonl # append findings to a JSONL audit log
36
+ KODELYTH_PI_GUARD_MAX_INPUT=20000 # truncate scanned text length
37
+ ```
38
+
39
+ ### Example
40
+
41
+ ```bash
42
+ # Warn-only (recommended for first rollout)
43
+ KODELYTH_PI_GUARD=warn claude
44
+
45
+ # Hard block on critical (production / enterprise)
46
+ KODELYTH_PI_GUARD=block KODELYTH_PI_GUARD_LOG=~/.kodelyth/safety/pi-audit.jsonl claude
47
+ ```
48
+
49
+ ---
50
+
51
+ ## 2. `token-budget.js`
52
+
53
+ Tracks per-session token usage and **blocks new turns** once a configurable budget is exceeded. Wires into:
54
+
55
+ - `SessionStart` — pre-check; can block if budget already exhausted
56
+ - `Stop` — accumulates usage from transcript size + visible message lengths
57
+
58
+ ### Token estimation
59
+
60
+ Hooks don't get authoritative token counts from the platform, so the budget enforcer uses a **4-chars-per-token heuristic** over the transcript path supplied in the Stop payload, plus visible prompt / tool-response text. Rough but stable, and good enough for guardrail purposes.
61
+
62
+ ### Modes
63
+
64
+ | `KODELYTH_TOKEN_BUDGET` | Behavior |
65
+ |---|---|
66
+ | `off` (or unset) | Hook does nothing. Default. |
67
+ | `warn` | Tracks usage and prints it to stderr after every turn. Never blocks. |
68
+ | `<positive integer>` (e.g. `200000`) | Hard budget in tokens. Warns at threshold. Blocks `SessionStart` once usage ≥ budget. |
69
+
70
+ ### Optional knobs
71
+
72
+ ```bash
73
+ KODELYTH_TOKEN_BUDGET_DIR=~/.kodelyth/safety # state file directory (per-session JSON)
74
+ KODELYTH_TOKEN_BUDGET_WARN=0.7 # warn at this fraction of budget (default 0.7)
75
+ KODELYTH_TOKEN_BUDGET_RESET=1 # admin op: wipe session state and exit
76
+ ```
77
+
78
+ ### Example
79
+
80
+ ```bash
81
+ # Warn-only — track but never block (good for cost visibility)
82
+ KODELYTH_TOKEN_BUDGET=warn claude
83
+
84
+ # Hard 200K-token budget per session, warn at 70%, block at 100%
85
+ KODELYTH_TOKEN_BUDGET=200000 claude
86
+
87
+ # Reset the current session's accumulated budget
88
+ KODELYTH_TOKEN_BUDGET=200000 KODELYTH_TOKEN_BUDGET_RESET=1 claude
89
+ ```
90
+
91
+ State files live at `${KODELYTH_TOKEN_BUDGET_DIR}/budget-<sessionId>.json`. Inspect or delete them manually anytime — the hook will recover gracefully.
92
+
93
+ ---
94
+
95
+ ## Why opt-in?
96
+
97
+ Safety primitives that **silently block traffic** are how observability tools become "that thing the team hates." Both hooks default to `off` so:
98
+
99
+ - Existing users upgrade to v1.7+ with zero behavior change.
100
+ - Adoption happens deliberately — `warn` first, `block` once the team trusts the signal.
101
+ - A misfire from a bad regex never silently kills a workflow.
102
+
103
+ Both hooks **always exit 0 on internal errors** (parse failure, missing env, fs error) so a broken hook never blocks the user.
104
+
105
+ ---
106
+
107
+ ## Architecture
108
+
109
+ ```
110
+ hooks/safety/
111
+ ├── README.md ← this file
112
+ ├── prompt-injection-guard.js ← UserPromptSubmit + PostToolUse hook
113
+ ├── token-budget.js ← SessionStart + Stop hook
114
+ └── lib/
115
+ └── patterns.js ← shared injection pattern catalog (regex + severity)
116
+ ```
117
+
118
+ Both hooks read the standard Claude Code hook payload from stdin, echo it back to stdout for chained hooks, and write any human-readable report to stderr. Tests cover block/warn/off paths plus parse-failure recovery.
119
+
120
+ Tests live at `tests/safety/`.
121
+
122
+ ---
123
+
124
+ Built into [Kodelyth ECC](../../README.md). MIT licensed.
@@ -0,0 +1,179 @@
1
+ // =============================================================================
2
+ // Kodelyth ECC — Safety Pattern Catalog
3
+ //
4
+ // Shared detection patterns for prompt-injection-guard and any other safety
5
+ // hook. Each pattern: { id, severity, regex, why }.
6
+ //
7
+ // Severity tiers:
8
+ // critical — strong override / system-prompt extraction / exfiltration
9
+ // high — clear injection markers (likely malicious in untrusted input)
10
+ // medium — suspicious context (could be legitimate, worth surfacing)
11
+ //
12
+ // Detection is regex-based + a couple of decoded checks. No LLM calls.
13
+ // =============================================================================
14
+
15
+ 'use strict';
16
+
17
+ // ── Regex catalog ────────────────────────────────────────────────────────────
18
+ const PATTERNS = [
19
+ // ── Critical: instruction override / system-prompt extraction ──────────────
20
+ {
21
+ id: 'override-prior-instructions',
22
+ severity: 'critical',
23
+ regex: /\b(ignore|disregard|forget)\s+(all|any|the|your|my|all\s+(?:prior|previous|above|earlier))?\s*(prior|previous|above|earlier|prev|preceding)?\s*(instructions?|rules?|prompt|system\s*prompt|directives?)\b/i,
24
+ why: 'Classic instruction-override prompt injection.',
25
+ },
26
+ {
27
+ id: 'system-prompt-leak',
28
+ severity: 'critical',
29
+ regex: /\b(reveal|print|show|repeat|output|dump|leak|expose)\s+(your|the|all|any)?\s*(system\s*prompt|hidden\s*prompt|initial\s*(prompt|instructions?)|original\s*(prompt|instructions?)|first\s*(message|prompt|instruction))/i,
30
+ why: 'Explicit attempt to extract the system prompt.',
31
+ },
32
+ {
33
+ id: 'role-rebind',
34
+ severity: 'critical',
35
+ regex: /\b(you\s+are\s+now|act\s+as|pretend\s+to\s+be|from\s+now\s+on\s+you\s+are|new\s+role:|switch\s+to\s+(?:dan|dev\s*mode|jailbreak|sudo|root|admin))\b/i,
36
+ why: 'Role-rebinding attempt (DAN, dev-mode, jailbreak personas).',
37
+ },
38
+ {
39
+ id: 'sudo-prefix',
40
+ severity: 'critical',
41
+ regex: /^\s*(?:sudo\s+|root:\s*|admin:\s*|override:\s*|jailbreak:\s*)/im,
42
+ why: 'Prefix attempting elevation/override.',
43
+ },
44
+ {
45
+ id: 'exfiltration-channel',
46
+ severity: 'critical',
47
+ regex: /\b(send|post|upload|exfiltrate|leak|forward)\s+(?:the\s+)?(secrets?|api[\s_-]?keys?|credentials?|env(?:ironment)?\s*(?:vars?|variables?)?|tokens?|sessions?|cookies?|chat\s+history|conversation)\s+(?:to|via|using)\b/i,
48
+ why: 'Tries to direct the model to exfiltrate sensitive data.',
49
+ },
50
+
51
+ // ── High: clear injection markers ──────────────────────────────────────────
52
+ {
53
+ id: 'hidden-system-marker',
54
+ severity: 'high',
55
+ regex: /(\[\[\s*(SYSTEM|INSTRUCTION|IMPORTANT|ADMIN|OVERRIDE)\s*[:>\]]|<\|\s*(system|im_start|admin|override)\s*\|>|^###\s*(SYSTEM|NEW\s+INSTRUCTIONS?|IMPORTANT|OVERRIDE)\b)/im,
56
+ why: 'Hidden-system markers commonly used in indirect injection.',
57
+ },
58
+ {
59
+ id: 'tool-call-hijack',
60
+ severity: 'high',
61
+ regex: /\b(call|invoke|run|execute)\s+(?:the\s+)?(tool|function|mcp|command|shell)\s+["'`]?(rm|curl|wget|fetch|bash|sh|exec|eval|delete_|drop_|exfil_|send_|post_|leak_)/i,
62
+ why: 'Attempts to hijack tool/function calls.',
63
+ },
64
+ {
65
+ id: 'jailbreak-canary',
66
+ severity: 'high',
67
+ regex: /\b(do\s+anything\s+now|dan|developer\s+mode\s+enabled|aim\s+mode|opposite\s+day|in\s+a\s+world\s+with\s+no\s+restrictions)\b/i,
68
+ why: 'Known jailbreak canary phrases.',
69
+ },
70
+ {
71
+ id: 'unrestricted-output',
72
+ severity: 'high',
73
+ regex: /\b(no\s+(filter|restrictions?|rules|safety|policies)|without\s+(any\s+)?(filter|restriction|safety|warning|disclaimer)|unfiltered\s+(answer|response))\b/i,
74
+ why: 'Demands an unrestricted / unfiltered response.',
75
+ },
76
+
77
+ // ── Medium: suspicious context (often false-positive in safe content) ──────
78
+ {
79
+ id: 'instructions-keyword',
80
+ severity: 'medium',
81
+ regex: /\b(new\s+instructions?|updated\s+instructions?|special\s+instructions?\s+for\s+you)\b/i,
82
+ why: 'Suspicious instruction-channel hijack attempt.',
83
+ },
84
+ {
85
+ id: 'invisible-character',
86
+ severity: 'medium',
87
+ regex: /[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/,
88
+ why: 'Invisible / bidi-override Unicode characters.',
89
+ },
90
+ {
91
+ id: 'huge-base64',
92
+ severity: 'medium',
93
+ regex: /[A-Za-z0-9+/=]{200,}/,
94
+ why: 'Large base64-looking blob — possible encoded payload.',
95
+ },
96
+ ];
97
+
98
+ // ── Decoded payload checks ───────────────────────────────────────────────────
99
+ // Decode any base64 substring >= 16 chars, search for jailbreak phrases inside.
100
+ const DECODE_PROBE = /\b[A-Za-z0-9+/]{16,}={0,2}\b/g;
101
+ const DECODED_RED_FLAGS = [
102
+ /ignore\s+(?:all\s+)?(?:prior|previous|above)\s+instructions/i,
103
+ /system\s*prompt/i,
104
+ /you\s+are\s+now/i,
105
+ /developer\s+mode/i,
106
+ ];
107
+
108
+ function checkDecoded(text) {
109
+ const findings = [];
110
+ if (typeof text !== 'string' || text.length < 16) return findings;
111
+ let m;
112
+ let count = 0;
113
+ DECODE_PROBE.lastIndex = 0;
114
+ while ((m = DECODE_PROBE.exec(text)) !== null && count < 20) {
115
+ count++;
116
+ const blob = m[0];
117
+ let decoded;
118
+ try { decoded = Buffer.from(blob, 'base64').toString('utf8'); }
119
+ catch { continue; }
120
+ // Skip random binary garbage — require >= 60% printable to look like text.
121
+ let printable = 0;
122
+ for (let i = 0; i < decoded.length; i++) {
123
+ const c = decoded.charCodeAt(i);
124
+ if ((c >= 0x20 && c <= 0x7e) || c === 0x09 || c === 0x0a) printable++;
125
+ }
126
+ if (decoded.length === 0 || printable / decoded.length < 0.6) continue;
127
+
128
+ for (const rx of DECODED_RED_FLAGS) {
129
+ if (rx.test(decoded)) {
130
+ findings.push({
131
+ id: 'decoded-payload',
132
+ severity: 'critical',
133
+ why: `Base64-decoded blob contains jailbreak phrase (${rx.source.slice(0, 40)}…).`,
134
+ excerpt: decoded.slice(0, 120),
135
+ });
136
+ break;
137
+ }
138
+ }
139
+ }
140
+ return findings;
141
+ }
142
+
143
+ // ── Public API ───────────────────────────────────────────────────────────────
144
+ function scan(text, { maxFindings = 10 } = {}) {
145
+ if (typeof text !== 'string' || text.length === 0) return [];
146
+ const findings = [];
147
+ for (const p of PATTERNS) {
148
+ if (findings.length >= maxFindings) break;
149
+ const m = p.regex.exec(text);
150
+ if (m) {
151
+ findings.push({
152
+ id: p.id,
153
+ severity: p.severity,
154
+ why: p.why,
155
+ excerpt: text.slice(Math.max(0, m.index - 20), m.index + m[0].length + 40),
156
+ });
157
+ }
158
+ }
159
+ for (const f of checkDecoded(text)) {
160
+ if (findings.length >= maxFindings) break;
161
+ findings.push(f);
162
+ }
163
+ return findings;
164
+ }
165
+
166
+ function maxSeverity(findings) {
167
+ if (!findings || findings.length === 0) return 'none';
168
+ if (findings.some(f => f.severity === 'critical')) return 'critical';
169
+ if (findings.some(f => f.severity === 'high')) return 'high';
170
+ if (findings.some(f => f.severity === 'medium')) return 'medium';
171
+ return 'none';
172
+ }
173
+
174
+ module.exports = {
175
+ PATTERNS,
176
+ scan,
177
+ checkDecoded,
178
+ maxSeverity,
179
+ };
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // Kodelyth ECC — Prompt Injection Guard
4
+ //
5
+ // A safety hook that scans inbound text for jailbreak / instruction-override
6
+ // patterns BEFORE the model sees it. Wires into two Claude Code hook events:
7
+ //
8
+ // - UserPromptSubmit (scans the user's outbound message)
9
+ // - PostToolUse on Read|WebFetch|mcp__* (scans tool responses for indirect
10
+ // injection from external content)
11
+ //
12
+ // Modes (env var KODELYTH_PI_GUARD):
13
+ // off — never run (default if unset)
14
+ // warn — exit 0 always; print findings to stderr (visible in transcript)
15
+ // block — exit 2 (block) on critical; warn on high; pass on medium
16
+ //
17
+ // Optional knobs:
18
+ // KODELYTH_PI_GUARD_LOG=/path/to/log.jsonl append findings as JSONL
19
+ // KODELYTH_PI_GUARD_MAX_INPUT=20000 truncate input before scan
20
+ //
21
+ // Always exits 0 on internal errors so the hook never blocks accidentally.
22
+ // =============================================================================
23
+
24
+ 'use strict';
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ const { scan, maxSeverity } = require('./lib/patterns');
30
+
31
+ const MODE = String(process.env.KODELYTH_PI_GUARD || 'off').toLowerCase();
32
+ const MAX_INPUT = Number(process.env.KODELYTH_PI_GUARD_MAX_INPUT || 20000);
33
+ const LOG_PATH = process.env.KODELYTH_PI_GUARD_LOG || null;
34
+
35
+ function safeExit(code) {
36
+ try { process.stdout.write(''); } catch {}
37
+ process.exit(code);
38
+ }
39
+
40
+ function appendLog(record) {
41
+ if (!LOG_PATH) return;
42
+ try {
43
+ fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
44
+ fs.appendFileSync(LOG_PATH, JSON.stringify(record) + '\n');
45
+ } catch { /* never block on logging */ }
46
+ }
47
+
48
+ // ── Read the hook payload from stdin ─────────────────────────────────────────
49
+ function readStdinSync() {
50
+ try {
51
+ const chunks = [];
52
+ let buf;
53
+ // Node 18+: readSync of stdin in blocking mode.
54
+ const fd = 0;
55
+ const tmp = Buffer.alloc(64 * 1024);
56
+ while (true) {
57
+ let n;
58
+ try { n = fs.readSync(fd, tmp, 0, tmp.length, null); }
59
+ catch { break; }
60
+ if (!n) break;
61
+ chunks.push(Buffer.from(tmp.subarray(0, n)));
62
+ }
63
+ return Buffer.concat(chunks).toString('utf8');
64
+ } catch {
65
+ return '';
66
+ }
67
+ }
68
+
69
+ // ── Extract scannable text from a hook payload ───────────────────────────────
70
+ function extractTextFromPayload(payload) {
71
+ if (!payload || typeof payload !== 'object') return '';
72
+ const buckets = [];
73
+
74
+ // Common payload shapes across Claude Code hooks:
75
+ // - UserPromptSubmit: { prompt: "..." } or { user_message: "..." }
76
+ // - PostToolUse: { tool_name, tool_response: { content: [...] } }
77
+ // - Generic fallback: any string anywhere
78
+
79
+ if (typeof payload.prompt === 'string') buckets.push(payload.prompt);
80
+ if (typeof payload.user_message === 'string') buckets.push(payload.user_message);
81
+ if (typeof payload.message === 'string') buckets.push(payload.message);
82
+
83
+ if (payload.tool_response) {
84
+ const tr = payload.tool_response;
85
+ if (typeof tr === 'string') buckets.push(tr);
86
+ if (Array.isArray(tr.content)) {
87
+ for (const c of tr.content) {
88
+ if (typeof c === 'string') buckets.push(c);
89
+ else if (c && typeof c.text === 'string') buckets.push(c.text);
90
+ }
91
+ } else if (typeof tr.text === 'string') {
92
+ buckets.push(tr.text);
93
+ } else if (typeof tr.output === 'string') {
94
+ buckets.push(tr.output);
95
+ }
96
+ }
97
+
98
+ // Last-resort fallback: walk shallowly for any string fields.
99
+ if (buckets.length === 0) {
100
+ for (const v of Object.values(payload)) {
101
+ if (typeof v === 'string') buckets.push(v);
102
+ }
103
+ }
104
+
105
+ return buckets.join('\n').slice(0, MAX_INPUT);
106
+ }
107
+
108
+ // ── Format a human-friendly stderr report ────────────────────────────────────
109
+ function formatReport(findings, eventLabel) {
110
+ const sev = maxSeverity(findings);
111
+ const lines = [];
112
+ lines.push(`\n[Kodelyth Safety] prompt-injection-guard — ${eventLabel} — severity=${sev}`);
113
+ for (const f of findings.slice(0, 5)) {
114
+ lines.push(` • [${f.severity}] ${f.id} — ${f.why}`);
115
+ if (f.excerpt) lines.push(` excerpt: ${f.excerpt.replace(/\s+/g, ' ').trim().slice(0, 120)}`);
116
+ }
117
+ if (findings.length > 5) lines.push(` …and ${findings.length - 5} more.`);
118
+ lines.push(` mode=${MODE}\n`);
119
+ return lines.join('\n');
120
+ }
121
+
122
+ // ── Main ─────────────────────────────────────────────────────────────────────
123
+ function main() {
124
+ if (MODE === 'off') return safeExit(0);
125
+
126
+ let raw = '';
127
+ try { raw = readStdinSync(); } catch { return safeExit(0); }
128
+
129
+ let payload = null;
130
+ try { payload = raw ? JSON.parse(raw) : null; } catch { payload = null; }
131
+
132
+ // Echo stdin to stdout so chained hooks/loggers still see the original.
133
+ if (raw) process.stdout.write(raw);
134
+
135
+ if (!payload) return safeExit(0);
136
+
137
+ const text = extractTextFromPayload(payload);
138
+ if (!text) return safeExit(0);
139
+
140
+ const findings = scan(text);
141
+ if (findings.length === 0) return safeExit(0);
142
+
143
+ const baseEvent = payload.hook_event_name || (payload.tool_name ? 'PostToolUse' : 'UserPromptSubmit');
144
+ const eventLabel = payload.tool_name ? `${baseEvent}:${payload.tool_name}` : baseEvent;
145
+
146
+ const sev = maxSeverity(findings);
147
+
148
+ appendLog({
149
+ timestamp: new Date().toISOString(),
150
+ event: eventLabel,
151
+ severity: sev,
152
+ mode: MODE,
153
+ findings: findings.map(f => ({ id: f.id, severity: f.severity, why: f.why })),
154
+ });
155
+
156
+ // Always emit the report on stderr (visible to user/transcript).
157
+ process.stderr.write(formatReport(findings, eventLabel));
158
+
159
+ if (MODE === 'block' && sev === 'critical') {
160
+ process.stderr.write(
161
+ '\n[Kodelyth Safety] BLOCKED: critical prompt-injection patterns detected.\n' +
162
+ ' • Set KODELYTH_PI_GUARD=warn to downgrade to warning-only.\n' +
163
+ ' • Set KODELYTH_PI_GUARD=off to disable this guard entirely.\n\n'
164
+ );
165
+ return safeExit(2);
166
+ }
167
+
168
+ return safeExit(0);
169
+ }
170
+
171
+ if (require.main === module) {
172
+ try { main(); } catch { safeExit(0); }
173
+ }
174
+
175
+ module.exports = {
176
+ // Exposed for tests
177
+ extractTextFromPayload,
178
+ formatReport,
179
+ };