claudeye 1.0.7 → 1.0.8

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/README.md CHANGED
@@ -160,7 +160,7 @@ claudeye --remove-hooks
160
160
 
161
161
  Selection is saved to `~/.claudeye/hooks-config.json`. In non-TTY environments (CI/piped), the 11 default policies are used automatically. Re-running `--install-hooks` re-opens the selector with your current choices pre-loaded.
162
162
 
163
- > **Web UI** — You can also toggle individual policies on/off from the **Hooks → Policies** tab in the Claudeye dashboard without re-running the CLI. Changes take effect immediately for the next hook invocation.
163
+ > **Web UI** — You can also toggle individual policies on/off from the **Policies** page in the Claudeye dashboard without re-running the CLI. Changes take effect immediately for the next hook invocation.
164
164
 
165
165
  ### Scoped Installation
166
166
 
@@ -217,6 +217,119 @@ claudeye --list-hooks --cwd /path/to/project
217
217
 
218
218
  **Avoid installing hooks in multiple scopes for the same project.** Claude Code merges settings from all scopes, so duplicate hooks may cause the same policy to evaluate twice. If `--list-hooks` shows hooks in multiple scopes, remove the extra installation with `--remove-hooks --scope <scope>`.
219
219
 
220
+ ### Policy Params
221
+
222
+ Tune builtin policies without replacing them. Add a `policyParams` key to any `.claudeye/hooks-config.json` file:
223
+
224
+ ```json
225
+ {
226
+ "enabledPolicies": ["block-sudo", "block-push-master", "warn-large-file-write"],
227
+ "policyParams": {
228
+ "block-sudo": {
229
+ "allowPatterns": ["sudo systemctl status *", "sudo journalctl *"]
230
+ },
231
+ "block-push-master": {
232
+ "protectedBranches": ["main", "master", "release"]
233
+ },
234
+ "warn-large-file-write": {
235
+ "thresholdKb": 512
236
+ }
237
+ }
238
+ }
239
+ ```
240
+
241
+ Config files are read from three scopes in priority order (first wins for params):
242
+ 1. `{cwd}/.claudeye/hooks-config.json` — project (can be committed)
243
+ 2. `{cwd}/.claudeye/hooks-config.local.json` — local (gitignore this)
244
+ 3. `~/.claudeye/hooks-config.json` — global (managed by `--install-hooks`)
245
+
246
+ `enabledPolicies` are unioned across all three scopes.
247
+
248
+ | Policy | Param | Type | Default | Description |
249
+ |---|---|---|---|---|
250
+ | `block-sudo` | `allowPatterns` | `string[]` | `[]` | Token-matched patterns to allow (e.g. `"sudo systemctl *"`) |
251
+ | `block-rm-rf` | `allowPaths` | `string[]` | `[]` | Paths exempt from catastrophic-deletion blocking |
252
+ | `block-read-outside-cwd` | `allowPaths` | `string[]` | `[]` | Paths outside cwd allowed for reading |
253
+ | `block-push-master` | `protectedBranches` | `string[]` | `["main","master"]` | Branch names blocked from `git push` |
254
+ | `block-work-on-main` | `protectedBranches` | `string[]` | `["main","master"]` | Branch names blocked for commits/merges |
255
+ | `sanitize-api-keys` | `additionalPatterns` | `{regex,label}[]` | `[]` | Extra credential patterns to redact from output |
256
+ | `block-secrets-write` | `additionalPatterns` | `string[]` | `[]` | Extra filename substrings to block writing |
257
+ | `warn-large-file-write` | `thresholdKb` | `number` | `1024` | Threshold in KB above which file writes warn |
258
+
259
+ > **`allowPatterns` safety** — `block-sudo` matches patterns against **parsed argv tokens**, not the raw command string. This prevents shell injection bypass like `sudo systemctl status; rm -rf /` from matching the pattern `sudo systemctl status *`.
260
+
261
+ > **Web UI** — Policy params can also be edited directly from the **Policies tab** in the dashboard. Click the gear icon next to any policy with configurable parameters to open the editor.
262
+
263
+ ### Custom Hooks
264
+
265
+ Register arbitrary hook logic in a JavaScript file using the `custom` keyword with `--install-hooks`:
266
+
267
+ ```bash
268
+ claudeye --install-hooks custom ./my-hooks.js
269
+ ```
270
+
271
+ **`my-hooks.js`**:
272
+ ```js
273
+ import { customPolicies, allow, deny, instruct } from "claudeye";
274
+
275
+ customPolicies.add({
276
+ name: "block-production-writes",
277
+ description: "Prevent writes to production config files",
278
+ match: { events: ["PreToolUse"] },
279
+ fn: async (ctx) => {
280
+ if (ctx.toolName === "Write") {
281
+ const path = ctx.toolInput?.file_path ?? "";
282
+ if (path.includes("production") || path.includes("prod.")) {
283
+ return deny("Writing to production config is blocked");
284
+ }
285
+ }
286
+ return allow();
287
+ },
288
+ });
289
+ ```
290
+
291
+ - **`ctx`** fields: `eventType`, `toolName`, `toolInput`, `payload`, `session`, `params`
292
+ - **Return values**: `allow()`, `deny(message)`, `instruct(message)` — `deny(message)` is surfaced to Claude as `"Blocked by claudeye: <message>"`, consistent with builtin policy output
293
+ - **Transitive imports**: files imported by your hooks entry point are automatically rewritten
294
+ - **Fail-open**: any error or 10-second timeout returns `allow` and logs a warning — Claude is never blocked by a broken hook
295
+ - **View loaded hooks**: `claudeye --list-hooks` shows a Custom Hooks section when a path is configured; the Policies tab in the dashboard shows each custom policy as a rich row with its name, description, and event scope (where defined), aligned with the built-in policy layout — edit the JS file to add, remove, or reorder them
296
+ - **Remove**: `claudeye --remove-hooks custom` clears the path from global config
297
+ - **Validation**: the file is loaded and validated at install time — if it has syntax errors, import failures, or registers no hooks, the install fails with an error and config is left unchanged
298
+
299
+ **TypeScript types** (exported from `claudeye`):
300
+
301
+ ```ts
302
+ interface PolicyContext {
303
+ eventType: string;
304
+ toolName?: string;
305
+ toolInput?: Record<string, unknown>;
306
+ payload: Record<string, unknown>;
307
+ session?: {
308
+ sessionId?: string;
309
+ transcriptPath?: string;
310
+ cwd?: string;
311
+ permissionMode?: string;
312
+ hookEventName?: string;
313
+ };
314
+ params?: Record<string, unknown>;
315
+ }
316
+
317
+ type PolicyDecision = "allow" | "deny" | "instruct";
318
+
319
+ interface PolicyResult {
320
+ decision: PolicyDecision;
321
+ reason?: string;
322
+ message?: string;
323
+ }
324
+
325
+ interface CustomHook {
326
+ name: string;
327
+ description?: string;
328
+ match?: { events?: HookEventType[] };
329
+ fn: (ctx: PolicyContext) => PolicyResult | Promise<PolicyResult>;
330
+ }
331
+ ```
332
+
220
333
  ### LLM-Powered Policies
221
334
 
222
335
  Some policies (like `verify-intent`) use an external LLM to make intelligent decisions. These require a one-time configuration of an OpenAI-compatible API provider.
@@ -271,7 +384,7 @@ claudeye --install-hooks verify-intent
271
384
 
272
385
  ### Hook Activity Dashboard
273
386
 
274
- Every policy evaluation is logged to `~/.claudeye/cache/hook-activity/` and displayed at `/hooks` in the dashboard. You can filter by decision (allow/deny), event type, and policy name — with pagination and auto-refresh.
387
+ Every policy evaluation is logged to `~/.claudeye/cache/hook-activity/` and displayed at `/policies` in the dashboard. You can filter by decision (allow/deny), event type, and policy name — with pagination and auto-refresh.
275
388
 
276
389
  Deny events are also reported to anonymous telemetry (if enabled) so you can track how often policies fire across machines.
277
390
 
@@ -390,7 +503,7 @@ The hook response format (`hookSpecificOutput` with `permissionDecision` and `pe
390
503
 
391
504
  #### Verification
392
505
 
393
- After installing hooks and configuring `setting_sources`, you can verify by checking the `/hooks` activity dashboard. Both Claude Code and Agent SDK hook events appear in the same activity log.
506
+ After installing hooks and configuring `setting_sources`, you can verify by checking the `/policies` activity dashboard. Both Claude Code and Agent SDK hook events appear in the same activity log.
394
507
 
395
508
  #### Common Pitfall
396
509
 
@@ -498,8 +611,8 @@ The free tier ships 13 built-in policies with more added in every update. For te
498
611
  | `--max-queue-items <num>` | Max items to enqueue per scan (0=unlimited) | `500` |
499
612
  | `--logging <level>` | Log level: `info`, `warn`, `error` (applies to dashboard server; hooks read `CLAUDEYE_LOG_LEVEL` env var) | `warn` |
500
613
  | `--disable-telemetry` | Disable anonymous usage analytics | enabled |
501
- | `--install-hooks [policies]` | Install hooks (interactive, or: `all`, `name1 name2 ...`) | - |
502
- | `--remove-hooks [policies]` | Remove hooks (all, or: `name1 name2 ...` to disable specific) | - |
614
+ | `--install-hooks [policies\|custom <path>]` | Install hooks (interactive, or: `all`, `name1 name2 ...`, or `custom <path>` to register a custom hooks JS file) | - |
615
+ | `--remove-hooks [policies\|custom]` | Remove hooks (all, or: `name1 name2 ...` to disable specific, or `custom` to clear the custom hooks path) | - |
503
616
  | `--list-hooks` | Show hook policies in a table with enabled status, descriptions, and a separate Beta section | - |
504
617
  | `--configure-llm` | Configure LLM provider for smart policies (interactive, or pass flags below) | - |
505
618
  | `--llm-api-key <key>` | API key for the LLM provider | - |
@@ -2398,6 +2511,7 @@ All views auto-refresh and self-hide when there's no queue activity.
2398
2511
  | `CLAUDEYE_QUEUE_MAX_ITEMS` | Max items to enqueue per scan (0=unlimited) | `500` |
2399
2512
  | `CLAUDEYE_LOG_LEVEL` | Log verbosity for both dashboard server and hook processes: `info`, `warn`, `error` | `warn` |
2400
2513
  | `CLAUDEYE_HOOK_LOG_FILE` | Enable hook file logging: `1` or `true` for default dir (`~/.claudeye/logs/`), or an absolute path for a custom directory | disabled |
2514
+ | `CLAUDEYE_DISABLE_PAGES` | Comma-separated pages to hide from nav and block direct access: `policies`, `dashboard`, `projects`. The first non-disabled page (policies → projects → dashboard) becomes the root `/` landing page. | unset |
2401
2515
 
2402
2516
  At `info` level, all log lines (including `ACTIVITY` lines for user actions) are emitted. At `warn` (default), only warnings and errors appear. At `error`, only errors are shown.
2403
2517
 
package/bin/claudeye.mjs CHANGED
@@ -52,10 +52,15 @@ if (platform() !== "win32") {
52
52
  try { chmodSync(binPath, 0o755); } catch {}
53
53
  }
54
54
 
55
+ // Tell the hook handler where to find dist/index.js (for custom hooks ESM shim)
56
+ const __dirname = dirname(fileURLToPath(import.meta.url));
57
+ const distPath = join(__dirname, "..", "dist");
58
+
55
59
  // Exec the binary with --mode=cli and forward all args
56
60
  try {
57
61
  execFileSync(binPath, ["--mode=cli", ...process.argv.slice(2)], {
58
62
  stdio: "inherit",
63
+ env: { ...process.env, CLAUDEYE_DIST_PATH: distPath },
59
64
  });
60
65
  } catch (err) {
61
66
  // execFileSync throws on non-zero exit — exit with the same code
package/dist/index.d.ts CHANGED
@@ -393,3 +393,59 @@ export interface ClaudeyeApp {
393
393
  export declare function createApp(): ClaudeyeApp;
394
394
  export declare function getQueueCondition(): { fn: ConditionFunction; cacheable: boolean } | null;
395
395
  export declare function clearQueueCondition(): void;
396
+
397
+ // ── Hooks API ──────────────────────────────────────────────────────────────
398
+
399
+ export type PolicyDecision = "allow" | "deny" | "instruct";
400
+
401
+ export type HookEventType =
402
+ | "SessionStart"
403
+ | "SessionEnd"
404
+ | "UserPromptSubmit"
405
+ | "PreToolUse"
406
+ | "PostToolUse"
407
+ | "PostToolUseFailure"
408
+ | "PermissionRequest"
409
+ | "Notification"
410
+ | "SubagentStart"
411
+ | "SubagentStop"
412
+ | "Stop"
413
+ | "TeammateIdle"
414
+ | "TaskCompleted"
415
+ | "ConfigChange"
416
+ | "WorktreeCreate"
417
+ | "WorktreeRemove"
418
+ | "PreCompact";
419
+
420
+ export interface PolicyResult {
421
+ decision: PolicyDecision;
422
+ reason?: string;
423
+ message?: string;
424
+ }
425
+
426
+ export interface PolicyContext {
427
+ eventType: string;
428
+ payload: Record<string, unknown>;
429
+ toolName?: string;
430
+ toolInput?: Record<string, unknown>;
431
+ session?: {
432
+ sessionId?: string;
433
+ transcriptPath?: string;
434
+ cwd?: string;
435
+ permissionMode?: string;
436
+ hookEventName?: string;
437
+ };
438
+ params?: Record<string, unknown>;
439
+ }
440
+
441
+ export interface CustomHook {
442
+ name: string;
443
+ description?: string;
444
+ match?: { events?: HookEventType[] };
445
+ fn: (ctx: PolicyContext) => PolicyResult | Promise<PolicyResult>;
446
+ }
447
+
448
+ export declare const customPolicies: { add(hook: CustomHook): void };
449
+ export declare function allow(): PolicyResult;
450
+ export declare function deny(reason: string): PolicyResult;
451
+ export declare function instruct(reason: string): PolicyResult;
package/dist/index.js CHANGED
@@ -463,8 +463,39 @@ function createApp() {
463
463
  return app;
464
464
  }
465
465
 
466
+ // ── Hooks API ──────────────────────────────────────────────────────────────
467
+
468
+ const CUSTOM_HOOKS_KEY = "__claudeye_custom_hooks__";
469
+
470
+ function getHooksRegistry() {
471
+ if (!Array.isArray(globalThis[CUSTOM_HOOKS_KEY])) globalThis[CUSTOM_HOOKS_KEY] = [];
472
+ return globalThis[CUSTOM_HOOKS_KEY];
473
+ }
474
+
475
+ const customPolicies = {
476
+ add(hook) {
477
+ getHooksRegistry().push(hook);
478
+ },
479
+ };
480
+
481
+ function allow() {
482
+ return { decision: "allow" };
483
+ }
484
+
485
+ function deny(reason) {
486
+ return { decision: "deny", reason };
487
+ }
488
+
489
+ function instruct(reason) {
490
+ return { decision: "instruct", reason };
491
+ }
492
+
466
493
  // ── Exports ────────────────────────────────────────────────────────────────
467
494
 
468
495
  exports.createApp = createApp;
469
496
  exports.getQueueCondition = getQueueCondition;
470
497
  exports.clearQueueCondition = clearQueueCondition;
498
+ exports.customPolicies = customPolicies;
499
+ exports.allow = allow;
500
+ exports.deny = deny;
501
+ exports.instruct = instruct;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeye",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Watchtower for Claude Code & Agents SDK — replay sessions, run custom evals, debug agent traces locally",
5
5
  "bin": {
6
6
  "claudeye": "./bin/claudeye.mjs"
@@ -61,10 +61,10 @@
61
61
  "access": "public"
62
62
  },
63
63
  "optionalDependencies": {
64
- "@claudeye/linux-x64": "1.0.7",
65
- "@claudeye/linux-arm64": "1.0.7",
66
- "@claudeye/darwin-x64": "1.0.7",
67
- "@claudeye/darwin-arm64": "1.0.7",
68
- "@claudeye/win32-x64": "1.0.7"
64
+ "@claudeye/linux-x64": "1.0.8",
65
+ "@claudeye/linux-arm64": "1.0.8",
66
+ "@claudeye/darwin-x64": "1.0.8",
67
+ "@claudeye/darwin-arm64": "1.0.8",
68
+ "@claudeye/win32-x64": "1.0.8"
69
69
  }
70
70
  }
@@ -2,8 +2,12 @@
2
2
  /**
3
3
  * preuninstall script for the claudeye wrapper package.
4
4
  *
5
- * Removes claudeye hook entries from Claude Code's ~/.claude/settings.json.
6
- * Does NOT touch ~/.claudeye/ (preserves cache, hooks-config, instance-id).
5
+ * Removes claudeye hook entries from all reachable Claude Code settings files:
6
+ * - ~/.claude/settings.json (user scope always attempted)
7
+ * - {cwd}/.claude/settings.json (project scope — if it exists)
8
+ * - {cwd}/.claude/settings.local.json (local scope — if it exists)
9
+ *
10
+ * Does NOT delete ~/.claudeye/ (preserves cache, hooks-config, instance-id).
7
11
  *
8
12
  * Never exits non-zero — uninstall must not be blocked by cleanup failures.
9
13
  * No external dependencies — Node.js built-ins only.
@@ -15,63 +19,101 @@ import { trackInstallEvent } from "./telemetry.mjs";
15
19
 
16
20
  const CLAUDEYE_HOOK_MARKER = "__claudeye_hook__";
17
21
 
18
- let removed = 0;
22
+ /**
23
+ * Remove all claudeye-marked hook entries from a single settings file.
24
+ * Returns the number of hook entries removed (not matchers).
25
+ * Writes the file only when at least one hook was removed.
26
+ */
27
+ function removeHooksFromFile(settingsPath) {
28
+ if (!existsSync(settingsPath)) return 0;
19
29
 
20
- try {
21
- const settingsPath = resolve(homedir(), ".claude", "settings.json");
30
+ let settings;
31
+ try {
32
+ settings = JSON.parse(readFileSync(settingsPath, "utf8"));
33
+ } catch {
34
+ return 0; // Corrupt or unreadable — nothing to do
35
+ }
36
+
37
+ if (!settings?.hooks) return 0;
38
+
39
+ let hooksRemoved = 0;
40
+
41
+ for (const eventType of Object.keys(settings.hooks)) {
42
+ const matchers = settings.hooks[eventType];
43
+ if (!Array.isArray(matchers)) continue;
44
+
45
+ for (let i = matchers.length - 1; i >= 0; i--) {
46
+ const matcher = matchers[i];
47
+ if (!matcher.hooks) continue;
48
+
49
+ const before = matcher.hooks.length;
50
+ matcher.hooks = matcher.hooks.filter((h) => {
51
+ if (h[CLAUDEYE_HOOK_MARKER] === true) return false; // marked entry
52
+ // Fallback for legacy installs that predate the marker
53
+ const cmd = typeof h.command === "string" ? h.command : "";
54
+ if (cmd.includes("claudeye") && cmd.includes("--hook")) return false;
55
+ return true;
56
+ });
57
+ hooksRemoved += before - matcher.hooks.length;
58
+
59
+ // Remove now-empty matchers
60
+ if (matcher.hooks.length === 0) {
61
+ matchers.splice(i, 1);
62
+ }
63
+ }
64
+
65
+ // Remove now-empty event type arrays
66
+ if (matchers.length === 0) {
67
+ delete settings.hooks[eventType];
68
+ }
69
+ }
70
+
71
+ // Remove now-empty hooks object
72
+ if (Object.keys(settings.hooks).length === 0) {
73
+ delete settings.hooks;
74
+ }
22
75
 
23
- if (existsSync(settingsPath)) {
24
- let settings;
25
- let parsed = false;
76
+ if (hooksRemoved > 0) {
26
77
  try {
27
- settings = JSON.parse(readFileSync(settingsPath, "utf8"));
28
- parsed = true;
78
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
29
79
  } catch {
30
- // Corrupt or unreadable nothing to do
80
+ // Best-effortdon't block uninstall
31
81
  }
82
+ }
32
83
 
33
- if (parsed && settings.hooks) {
34
- for (const eventType of Object.keys(settings.hooks)) {
35
- const matchers = settings.hooks[eventType];
36
- if (!Array.isArray(matchers)) continue;
37
-
38
- for (let i = matchers.length - 1; i >= 0; i--) {
39
- const matcher = matchers[i];
40
- if (!matcher.hooks) continue;
41
-
42
- matcher.hooks = matcher.hooks.filter(
43
- (h) => h[CLAUDEYE_HOOK_MARKER] !== true
44
- );
45
-
46
- // Remove empty matchers
47
- if (matcher.hooks.length === 0) {
48
- matchers.splice(i, 1);
49
- removed++;
50
- }
51
- }
52
-
53
- // Remove empty event type arrays
54
- if (matchers.length === 0) {
55
- delete settings.hooks[eventType];
56
- }
57
- }
84
+ return hooksRemoved;
85
+ }
58
86
 
59
- // Remove empty hooks object
60
- if (Object.keys(settings.hooks).length === 0) {
61
- delete settings.hooks;
62
- }
87
+ let totalRemoved = 0;
63
88
 
64
- if (removed > 0) {
65
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
66
- console.log(`[claudeye] Removed ${removed} hook(s) from ~/.claude/settings.json.`);
67
- }
89
+ try {
90
+ const home = homedir();
91
+ const projectCwd = process.cwd();
68
92
 
69
- console.log(
70
- `[claudeye] Note: project/local scope hooks must be removed manually if they were installed.\n` +
71
- ` Run: claudeye --remove-hooks --scope project (or --scope local)`
72
- );
93
+ // Build list of settings files to clean, deduped in case cwd === home
94
+ const candidates = [
95
+ resolve(home, ".claude", "settings.json"), // user scope
96
+ resolve(projectCwd, ".claude", "settings.json"), // project scope
97
+ resolve(projectCwd, ".claude", "settings.local.json"), // local scope
98
+ ];
99
+ const seen = new Set();
100
+ const settingsPaths = candidates.filter((p) => {
101
+ if (seen.has(p)) return false;
102
+ seen.add(p);
103
+ return true;
104
+ });
105
+
106
+ for (const settingsPath of settingsPaths) {
107
+ const removed = removeHooksFromFile(settingsPath);
108
+ if (removed > 0) {
109
+ console.log(`[claudeye] Removed ${removed} hook(s) from ${settingsPath}.`);
110
+ totalRemoved += removed;
73
111
  }
74
112
  }
113
+
114
+ if (totalRemoved === 0) {
115
+ console.log("[claudeye] No hook entries found to remove.");
116
+ }
75
117
  } catch {
76
118
  // Never block uninstall
77
119
  }
@@ -81,6 +123,6 @@ try {
81
123
  await trackInstallEvent("package_uninstalled", {
82
124
  platform: platform(),
83
125
  arch: arch(),
84
- hooks_removed: removed,
126
+ hooks_removed: totalRemoved,
85
127
  });
86
128
  } catch {}