@melihmucuk/leash 1.0.2 → 1.0.4

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
@@ -19,26 +19,27 @@ AI agents can hallucinate dangerous commands. Leash sandboxes them:
19
19
  ## Quick Start
20
20
 
21
21
  ```bash
22
+ # Install leash globally
22
23
  npm install -g @melihmucuk/leash
24
+
25
+ # Setup leash for your platform
23
26
  leash --setup <platform>
24
- ```
25
27
 
26
- | Platform | Command |
27
- |----------|---------|
28
- | OpenCode | `leash --setup opencode` |
29
- | Pi Coding Agent | `leash --setup pi` |
30
- | Claude Code | `leash --setup claude-code` |
31
- | Factory Droid | `leash --setup factory` |
28
+ # Remove leash from a platform
29
+ leash --remove <platform>
32
30
 
33
- Restart your agent. Done!
31
+ # Update leash anytime
32
+ leash --update
33
+ ```
34
34
 
35
- ```bash
36
- # Update anytime
37
- npm update -g @melihmucuk/leash
35
+ | Platform | Command |
36
+ | --------------- | --------------------------- |
37
+ | OpenCode | `leash --setup opencode` |
38
+ | Pi Coding Agent | `leash --setup pi` |
39
+ | Claude Code | `leash --setup claude-code` |
40
+ | Factory Droid | `leash --setup factory` |
38
41
 
39
- # Remove from a platform
40
- leash --remove <platform>
41
- ```
42
+ Restart your agent. Done!
42
43
 
43
44
  <details>
44
45
  <summary><b>Manual Setup</b></summary>
@@ -72,6 +73,16 @@ Add to `~/.claude/settings.json`:
72
73
  ```json
73
74
  {
74
75
  "hooks": {
76
+ "SessionStart": [
77
+ {
78
+ "hooks": [
79
+ {
80
+ "type": "command",
81
+ "command": "node <path from leash --path claude-code>"
82
+ }
83
+ ]
84
+ }
85
+ ],
75
86
  "PreToolUse": [
76
87
  {
77
88
  "matcher": "Bash|Write|Edit",
@@ -94,6 +105,16 @@ Add to `~/.factory/settings.json`:
94
105
  ```json
95
106
  {
96
107
  "hooks": {
108
+ "SessionStart": [
109
+ {
110
+ "hooks": [
111
+ {
112
+ "type": "command",
113
+ "command": "node <path from leash --path factory>"
114
+ }
115
+ ]
116
+ }
117
+ ],
97
118
  "PreToolUse": [
98
119
  {
99
120
  "matcher": "Execute|Write|Edit",
package/bin/leash.js CHANGED
@@ -4,7 +4,9 @@ import { existsSync } from "fs";
4
4
  import { dirname, join } from "path";
5
5
  import { homedir } from "os";
6
6
  import { fileURLToPath } from "url";
7
+ import { execSync } from "child_process";
7
8
  import { PLATFORMS, setupPlatform, removePlatform } from "./lib.js";
9
+ import { checkForUpdates } from "../packages/core/lib/version-checker.js";
8
10
 
9
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
12
 
@@ -107,6 +109,28 @@ function showPath(platformKey) {
107
109
  console.log(leashPath);
108
110
  }
109
111
 
112
+ async function update() {
113
+ console.log("Checking for updates...");
114
+
115
+ const result = await checkForUpdates();
116
+
117
+ if (!result.hasUpdate) {
118
+ console.log(`[ok] Already up to date (v${result.currentVersion})`);
119
+ return;
120
+ }
121
+
122
+ console.log(`[ok] Update available: v${result.currentVersion} → v${result.latestVersion}`);
123
+ console.log("[ok] Updating...");
124
+
125
+ try {
126
+ execSync("npm update -g @melihmucuk/leash", { stdio: "inherit" });
127
+ console.log("[ok] Update complete");
128
+ } catch {
129
+ console.error("[error] Update failed. Try manually: npm update -g @melihmucuk/leash");
130
+ process.exit(1);
131
+ }
132
+ }
133
+
110
134
  function showHelp() {
111
135
  console.log(`
112
136
  leash - Security guardrails for AI coding agents
@@ -115,6 +139,7 @@ Usage:
115
139
  leash --setup <platform> Install leash for a platform
116
140
  leash --remove <platform> Remove leash from a platform
117
141
  leash --path <platform> Show leash path for a platform
142
+ leash --update Update leash to latest version
118
143
  leash --help Show this help
119
144
 
120
145
  Platforms:
@@ -127,6 +152,7 @@ Examples:
127
152
  leash --setup opencode
128
153
  leash --remove claude-code
129
154
  leash --path pi
155
+ leash --update
130
156
  `);
131
157
  }
132
158
 
@@ -162,6 +188,10 @@ switch (command) {
162
188
  }
163
189
  showPath(platform);
164
190
  break;
191
+ case "--update":
192
+ case "-u":
193
+ await update();
194
+ break;
165
195
  case "--help":
166
196
  case "-h":
167
197
  case undefined:
package/bin/lib.js CHANGED
@@ -49,26 +49,59 @@ export const PLATFORMS = {
49
49
  distPath: "claude-code/leash.js",
50
50
  setup: (config, leashPath) => {
51
51
  config.hooks = config.hooks || {};
52
- config.hooks.PreToolUse = config.hooks.PreToolUse || [];
53
- const exists = config.hooks.PreToolUse.some((entry) =>
52
+ const hookCommand = { type: "command", command: `node ${leashPath}` };
53
+
54
+ // Check if already installed in either hook
55
+ const inSessionStart = config.hooks.SessionStart?.some((entry) =>
56
+ entry.hooks?.some((h) => h.command?.includes("leash"))
57
+ );
58
+ const inPreToolUse = config.hooks.PreToolUse?.some((entry) =>
54
59
  entry.hooks?.some((h) => h.command?.includes("leash"))
55
60
  );
56
- if (exists) {
61
+ if (inSessionStart && inPreToolUse) {
57
62
  return { skipped: true };
58
63
  }
59
- config.hooks.PreToolUse.push({
60
- matcher: "Bash|Write|Edit",
61
- hooks: [{ type: "command", command: `node ${leashPath}` }],
62
- });
64
+
65
+ // Add SessionStart hook
66
+ if (!inSessionStart) {
67
+ config.hooks.SessionStart = config.hooks.SessionStart || [];
68
+ config.hooks.SessionStart.push({
69
+ hooks: [hookCommand],
70
+ });
71
+ }
72
+
73
+ // Add PreToolUse hook
74
+ if (!inPreToolUse) {
75
+ config.hooks.PreToolUse = config.hooks.PreToolUse || [];
76
+ config.hooks.PreToolUse.push({
77
+ matcher: "Bash|Write|Edit",
78
+ hooks: [hookCommand],
79
+ });
80
+ }
81
+
63
82
  return { skipped: false };
64
83
  },
65
84
  remove: (config) => {
66
- if (!config.hooks?.PreToolUse) return false;
67
- const before = config.hooks.PreToolUse.length;
68
- config.hooks.PreToolUse = config.hooks.PreToolUse.filter(
69
- (entry) => !entry.hooks?.some((h) => h.command?.includes("leash"))
70
- );
71
- return config.hooks.PreToolUse.length < before;
85
+ if (!config.hooks) return false;
86
+ let removed = false;
87
+
88
+ if (config.hooks.SessionStart) {
89
+ const before = config.hooks.SessionStart.length;
90
+ config.hooks.SessionStart = config.hooks.SessionStart.filter(
91
+ (entry) => !entry.hooks?.some((h) => h.command?.includes("leash"))
92
+ );
93
+ if (config.hooks.SessionStart.length < before) removed = true;
94
+ }
95
+
96
+ if (config.hooks.PreToolUse) {
97
+ const before = config.hooks.PreToolUse.length;
98
+ config.hooks.PreToolUse = config.hooks.PreToolUse.filter(
99
+ (entry) => !entry.hooks?.some((h) => h.command?.includes("leash"))
100
+ );
101
+ if (config.hooks.PreToolUse.length < before) removed = true;
102
+ }
103
+
104
+ return removed;
72
105
  },
73
106
  },
74
107
  factory: {
@@ -77,26 +110,59 @@ export const PLATFORMS = {
77
110
  distPath: "factory/leash.js",
78
111
  setup: (config, leashPath) => {
79
112
  config.hooks = config.hooks || {};
80
- config.hooks.PreToolUse = config.hooks.PreToolUse || [];
81
- const exists = config.hooks.PreToolUse.some((entry) =>
113
+ const hookCommand = { type: "command", command: `node ${leashPath}` };
114
+
115
+ // Check if already installed in either hook
116
+ const inSessionStart = config.hooks.SessionStart?.some((entry) =>
117
+ entry.hooks?.some((h) => h.command?.includes("leash"))
118
+ );
119
+ const inPreToolUse = config.hooks.PreToolUse?.some((entry) =>
82
120
  entry.hooks?.some((h) => h.command?.includes("leash"))
83
121
  );
84
- if (exists) {
122
+ if (inSessionStart && inPreToolUse) {
85
123
  return { skipped: true };
86
124
  }
87
- config.hooks.PreToolUse.push({
88
- matcher: "Execute|Write|Edit",
89
- hooks: [{ type: "command", command: `node ${leashPath}` }],
90
- });
125
+
126
+ // Add SessionStart hook
127
+ if (!inSessionStart) {
128
+ config.hooks.SessionStart = config.hooks.SessionStart || [];
129
+ config.hooks.SessionStart.push({
130
+ hooks: [hookCommand],
131
+ });
132
+ }
133
+
134
+ // Add PreToolUse hook
135
+ if (!inPreToolUse) {
136
+ config.hooks.PreToolUse = config.hooks.PreToolUse || [];
137
+ config.hooks.PreToolUse.push({
138
+ matcher: "Execute|Write|Edit",
139
+ hooks: [hookCommand],
140
+ });
141
+ }
142
+
91
143
  return { skipped: false };
92
144
  },
93
145
  remove: (config) => {
94
- if (!config.hooks?.PreToolUse) return false;
95
- const before = config.hooks.PreToolUse.length;
96
- config.hooks.PreToolUse = config.hooks.PreToolUse.filter(
97
- (entry) => !entry.hooks?.some((h) => h.command?.includes("leash"))
98
- );
99
- return config.hooks.PreToolUse.length < before;
146
+ if (!config.hooks) return false;
147
+ let removed = false;
148
+
149
+ if (config.hooks.SessionStart) {
150
+ const before = config.hooks.SessionStart.length;
151
+ config.hooks.SessionStart = config.hooks.SessionStart.filter(
152
+ (entry) => !entry.hooks?.some((h) => h.command?.includes("leash"))
153
+ );
154
+ if (config.hooks.SessionStart.length < before) removed = true;
155
+ }
156
+
157
+ if (config.hooks.PreToolUse) {
158
+ const before = config.hooks.PreToolUse.length;
159
+ config.hooks.PreToolUse = config.hooks.PreToolUse.filter(
160
+ (entry) => !entry.hooks?.some((h) => h.command?.includes("leash"))
161
+ );
162
+ if (config.hooks.PreToolUse.length < before) removed = true;
163
+ }
164
+
165
+ return removed;
100
166
  },
101
167
  },
102
168
  };
@@ -466,6 +466,48 @@ var CommandAnalyzer = class {
466
466
  }
467
467
  };
468
468
 
469
+ // packages/core/version-checker.ts
470
+ import { readFileSync, existsSync } from "fs";
471
+ import { dirname, join } from "path";
472
+ import { fileURLToPath } from "url";
473
+ function getVersion() {
474
+ const __dirname = dirname(fileURLToPath(import.meta.url));
475
+ const candidates = [
476
+ join(__dirname, "..", "..", "package.json"),
477
+ join(__dirname, "..", "..", "..", "package.json")
478
+ ];
479
+ for (const path of candidates) {
480
+ if (existsSync(path)) {
481
+ try {
482
+ const pkg = JSON.parse(readFileSync(path, "utf-8"));
483
+ if (pkg.name === "@melihmucuk/leash") {
484
+ return pkg.version;
485
+ }
486
+ } catch {
487
+ }
488
+ }
489
+ }
490
+ return "0.0.0";
491
+ }
492
+ var CURRENT_VERSION = getVersion();
493
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@melihmucuk/leash/latest";
494
+ async function checkForUpdates() {
495
+ try {
496
+ const response = await fetch(NPM_REGISTRY_URL);
497
+ if (!response.ok) {
498
+ return { hasUpdate: false, currentVersion: CURRENT_VERSION };
499
+ }
500
+ const data = await response.json();
501
+ return {
502
+ hasUpdate: data.version !== CURRENT_VERSION,
503
+ latestVersion: data.version,
504
+ currentVersion: CURRENT_VERSION
505
+ };
506
+ } catch {
507
+ return { hasUpdate: false, currentVersion: CURRENT_VERSION };
508
+ }
509
+ }
510
+
469
511
  // packages/claude-code/leash.ts
470
512
  async function readStdin() {
471
513
  const chunks = [];
@@ -483,10 +525,21 @@ async function main() {
483
525
  console.error("Failed to parse input JSON");
484
526
  process.exit(1);
485
527
  }
486
- const { tool_name, tool_input, cwd } = input;
528
+ const { hook_event_name, tool_name, tool_input, cwd } = input;
529
+ if (hook_event_name === "SessionStart") {
530
+ const messages = ["\u{1F512} Leash active"];
531
+ const update = await checkForUpdates();
532
+ if (update.hasUpdate) {
533
+ messages.push(
534
+ `\u{1F504} Leash ${update.latestVersion} available. Run: leash --update`
535
+ );
536
+ }
537
+ console.log(JSON.stringify({ systemMessage: messages.join("\n") }));
538
+ process.exit(0);
539
+ }
487
540
  const analyzer = new CommandAnalyzer(cwd);
488
541
  if (tool_name === "Bash") {
489
- const command = tool_input.command || "";
542
+ const command = tool_input?.command || "";
490
543
  const result = analyzer.analyze(command);
491
544
  if (result.blocked) {
492
545
  console.error(
@@ -499,7 +552,7 @@ Action: Guide the user to run the command manually.`
499
552
  }
500
553
  }
501
554
  if (tool_name === "Write" || tool_name === "Edit") {
502
- const path = tool_input.file_path || "";
555
+ const path = tool_input?.file_path || "";
503
556
  const result = analyzer.validatePath(path);
504
557
  if (result.blocked) {
505
558
  console.error(
@@ -466,6 +466,48 @@ var CommandAnalyzer = class {
466
466
  }
467
467
  };
468
468
 
469
+ // packages/core/version-checker.ts
470
+ import { readFileSync, existsSync } from "fs";
471
+ import { dirname, join } from "path";
472
+ import { fileURLToPath } from "url";
473
+ function getVersion() {
474
+ const __dirname = dirname(fileURLToPath(import.meta.url));
475
+ const candidates = [
476
+ join(__dirname, "..", "..", "package.json"),
477
+ join(__dirname, "..", "..", "..", "package.json")
478
+ ];
479
+ for (const path of candidates) {
480
+ if (existsSync(path)) {
481
+ try {
482
+ const pkg = JSON.parse(readFileSync(path, "utf-8"));
483
+ if (pkg.name === "@melihmucuk/leash") {
484
+ return pkg.version;
485
+ }
486
+ } catch {
487
+ }
488
+ }
489
+ }
490
+ return "0.0.0";
491
+ }
492
+ var CURRENT_VERSION = getVersion();
493
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@melihmucuk/leash/latest";
494
+ async function checkForUpdates() {
495
+ try {
496
+ const response = await fetch(NPM_REGISTRY_URL);
497
+ if (!response.ok) {
498
+ return { hasUpdate: false, currentVersion: CURRENT_VERSION };
499
+ }
500
+ const data = await response.json();
501
+ return {
502
+ hasUpdate: data.version !== CURRENT_VERSION,
503
+ latestVersion: data.version,
504
+ currentVersion: CURRENT_VERSION
505
+ };
506
+ } catch {
507
+ return { hasUpdate: false, currentVersion: CURRENT_VERSION };
508
+ }
509
+ }
510
+
469
511
  // packages/factory/leash.ts
470
512
  async function readStdin() {
471
513
  const chunks = [];
@@ -483,11 +525,22 @@ async function main() {
483
525
  console.error("Failed to parse input JSON");
484
526
  process.exit(1);
485
527
  }
486
- const { tool_name, tool_input } = input;
528
+ const { hook_event_name, tool_name, tool_input } = input;
487
529
  const cwd = process.env.FACTORY_PROJECT_DIR || input.cwd || process.cwd();
530
+ if (hook_event_name === "SessionStart") {
531
+ const messages = ["\u{1F512} Leash active"];
532
+ const update = await checkForUpdates();
533
+ if (update.hasUpdate) {
534
+ messages.push(
535
+ `\u{1F504} Leash ${update.latestVersion} available. Run: leash --update`
536
+ );
537
+ }
538
+ console.log(JSON.stringify({ systemMessage: messages.join("\n") }));
539
+ process.exit(0);
540
+ }
488
541
  const analyzer = new CommandAnalyzer(cwd);
489
542
  if (tool_name === "Execute") {
490
- const command = tool_input.command || "";
543
+ const command = tool_input?.command || "";
491
544
  const result = analyzer.analyze(command);
492
545
  if (result.blocked) {
493
546
  console.error(
@@ -500,7 +553,7 @@ Action: Guide the user to run the command manually.`
500
553
  }
501
554
  }
502
555
  if (tool_name === "Write" || tool_name === "Edit") {
503
- const path = tool_input.file_path || "";
556
+ const path = tool_input?.file_path || "";
504
557
  const result = analyzer.validatePath(path);
505
558
  if (result.blocked) {
506
559
  console.error(
@@ -464,6 +464,48 @@ var CommandAnalyzer = class {
464
464
  }
465
465
  };
466
466
 
467
+ // packages/core/version-checker.ts
468
+ import { readFileSync, existsSync } from "fs";
469
+ import { dirname, join } from "path";
470
+ import { fileURLToPath } from "url";
471
+ function getVersion() {
472
+ const __dirname = dirname(fileURLToPath(import.meta.url));
473
+ const candidates = [
474
+ join(__dirname, "..", "..", "package.json"),
475
+ join(__dirname, "..", "..", "..", "package.json")
476
+ ];
477
+ for (const path of candidates) {
478
+ if (existsSync(path)) {
479
+ try {
480
+ const pkg = JSON.parse(readFileSync(path, "utf-8"));
481
+ if (pkg.name === "@melihmucuk/leash") {
482
+ return pkg.version;
483
+ }
484
+ } catch {
485
+ }
486
+ }
487
+ }
488
+ return "0.0.0";
489
+ }
490
+ var CURRENT_VERSION = getVersion();
491
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@melihmucuk/leash/latest";
492
+ async function checkForUpdates() {
493
+ try {
494
+ const response = await fetch(NPM_REGISTRY_URL);
495
+ if (!response.ok) {
496
+ return { hasUpdate: false, currentVersion: CURRENT_VERSION };
497
+ }
498
+ const data = await response.json();
499
+ return {
500
+ hasUpdate: data.version !== CURRENT_VERSION,
501
+ latestVersion: data.version,
502
+ currentVersion: CURRENT_VERSION
503
+ };
504
+ } catch {
505
+ return { hasUpdate: false, currentVersion: CURRENT_VERSION };
506
+ }
507
+ }
508
+
467
509
  // packages/opencode/leash.ts
468
510
  var Leash = async ({ directory, client }) => {
469
511
  const analyzer = new CommandAnalyzer(directory);
@@ -473,6 +515,16 @@ var Leash = async ({ directory, client }) => {
473
515
  await client.tui.showToast({
474
516
  body: { message: "\u{1F512} Leash active", variant: "info" }
475
517
  });
518
+ const update = await checkForUpdates();
519
+ if (update.hasUpdate) {
520
+ await client.tui.showToast({
521
+ body: {
522
+ message: `\u{1F504} Leash ${update.latestVersion} available.
523
+ Run: leash --update (restart required)`,
524
+ variant: "warning"
525
+ }
526
+ });
527
+ }
476
528
  }
477
529
  },
478
530
  "tool.execute.before": async (input, output) => {
package/dist/pi/leash.js CHANGED
@@ -464,6 +464,48 @@ var CommandAnalyzer = class {
464
464
  }
465
465
  };
466
466
 
467
+ // packages/core/version-checker.ts
468
+ import { readFileSync, existsSync } from "fs";
469
+ import { dirname, join } from "path";
470
+ import { fileURLToPath } from "url";
471
+ function getVersion() {
472
+ const __dirname = dirname(fileURLToPath(import.meta.url));
473
+ const candidates = [
474
+ join(__dirname, "..", "..", "package.json"),
475
+ join(__dirname, "..", "..", "..", "package.json")
476
+ ];
477
+ for (const path of candidates) {
478
+ if (existsSync(path)) {
479
+ try {
480
+ const pkg = JSON.parse(readFileSync(path, "utf-8"));
481
+ if (pkg.name === "@melihmucuk/leash") {
482
+ return pkg.version;
483
+ }
484
+ } catch {
485
+ }
486
+ }
487
+ }
488
+ return "0.0.0";
489
+ }
490
+ var CURRENT_VERSION = getVersion();
491
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@melihmucuk/leash/latest";
492
+ async function checkForUpdates() {
493
+ try {
494
+ const response = await fetch(NPM_REGISTRY_URL);
495
+ if (!response.ok) {
496
+ return { hasUpdate: false, currentVersion: CURRENT_VERSION };
497
+ }
498
+ const data = await response.json();
499
+ return {
500
+ hasUpdate: data.version !== CURRENT_VERSION,
501
+ latestVersion: data.version,
502
+ currentVersion: CURRENT_VERSION
503
+ };
504
+ } catch {
505
+ return { hasUpdate: false, currentVersion: CURRENT_VERSION };
506
+ }
507
+ }
508
+
467
509
  // packages/pi/leash.ts
468
510
  function leash_default(pi) {
469
511
  let analyzer = null;
@@ -471,6 +513,13 @@ function leash_default(pi) {
471
513
  if (event.reason === "start") {
472
514
  analyzer = new CommandAnalyzer(ctx.cwd);
473
515
  ctx.ui.notify("\u{1F512} Leash active", "info");
516
+ const update = await checkForUpdates();
517
+ if (update.hasUpdate) {
518
+ ctx.ui.notify(
519
+ `\u{1F504} Leash ${update.latestVersion} available. Run: leash --update (restart required)`,
520
+ "warning"
521
+ );
522
+ }
474
523
  }
475
524
  });
476
525
  pi.on("tool_call", async (event, ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melihmucuk/leash",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
5
  "description": "Security guardrails for AI coding agents",
6
6
  "bin": {