@melihmucuk/leash 1.0.7 → 1.0.9

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
@@ -23,6 +23,7 @@ AI agents can hallucinate dangerous commands. Leash sandboxes them:
23
23
  <img height="400" alt="image" src="https://github.com/user-attachments/assets/94f0a4e5-db6c-4b14-bddd-b8984c51ed3d" />
24
24
 
25
25
  Links:
26
+
26
27
  1. [Claude CLI deleted my entire home directory (Dec 8th 2025)](https://www.reddit.com/r/ClaudeAI/comments/1pgxckk/claude_cli_deleted_my_entire_home_directory_wiped/)
27
28
  2. [Google Antigravity just deleted my drive (Nov 27th 2025)](https://www.reddit.com/r/google_antigravity/comments/1p82or6/google_antigravity_just_deleted_the_contents_of/)
28
29
 
@@ -56,13 +57,13 @@ Restart your agent. Done!
56
57
 
57
58
  If you prefer manual configuration, use `leash --path <platform>` to get the path and add it to your config file.
58
59
 
59
- **Pi Coding Agent** - [docs](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/hooks.md)
60
+ **Pi Coding Agent** - [docs](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md)
60
61
 
61
62
  Add to `~/.pi/agent/settings.json`:
62
63
 
63
64
  ```json
64
65
  {
65
- "hooks": ["<path from leash --path pi>"]
66
+ "extensions": ["<path from leash --path pi>"]
66
67
  }
67
68
  ```
68
69
 
@@ -328,7 +329,7 @@ Near-zero latency impact on your workflow:
328
329
  | Platform | Latency per tool call | Notes |
329
330
  | ----------- | --------------------- | ---------------------------------------- |
330
331
  | OpenCode | **~20µs** | In-process plugin, near-zero overhead |
331
- | Pi | **~20µs** | In-process hook, near-zero overhead |
332
+ | Pi | **~20µs** | In-process extension, near-zero overhead |
332
333
  | Claude Code | **~31ms** | External process (~30ms Node.js startup) |
333
334
  | Factory | **~31ms** | External process (~30ms Node.js startup) |
334
335
 
package/bin/lib.js CHANGED
@@ -29,18 +29,18 @@ export const PLATFORMS = {
29
29
  configPath: ".pi/agent/settings.json",
30
30
  distPath: "pi/leash.js",
31
31
  setup: (config, leashPath) => {
32
- config.hooks = config.hooks || [];
33
- if (config.hooks.some((h) => h.includes("leash"))) {
32
+ config.extensions = config.extensions || [];
33
+ if (config.extensions.some((e) => e.includes("leash"))) {
34
34
  return { skipped: true };
35
35
  }
36
- config.hooks.push(leashPath);
36
+ config.extensions.push(leashPath);
37
37
  return { skipped: false };
38
38
  },
39
39
  remove: (config) => {
40
- if (!config.hooks) return false;
41
- const before = config.hooks.length;
42
- config.hooks = config.hooks.filter((h) => !h.includes("leash"));
43
- return config.hooks.length < before;
40
+ if (!config.extensions) return false;
41
+ const before = config.extensions.length;
42
+ config.extensions = config.extensions.filter((e) => !e.includes("leash"));
43
+ return config.extensions.length < before;
44
44
  },
45
45
  },
46
46
  "claude-code": {
@@ -347,7 +347,11 @@ var CommandAnalyzer = class {
347
347
  reason: `Command "${name}" targets path outside working directory: ${path}`
348
348
  };
349
349
  }
350
- const result = this.checkProtectedPath(path, `Command "${name}"`, resolveBase);
350
+ const result = this.checkProtectedPath(
351
+ path,
352
+ `Command "${name}"`,
353
+ resolveBase
354
+ );
351
355
  if (result.blocked) return result;
352
356
  }
353
357
  }
@@ -359,7 +363,10 @@ var CommandAnalyzer = class {
359
363
  if (!this.isPathAllowed(dest, true, resolveBase)) {
360
364
  return {
361
365
  blocked: true,
362
- reason: `Command "cp" targets path outside working directory: ${this.resolvePath(dest, resolveBase)}`
366
+ reason: `Command "cp" targets path outside working directory: ${this.resolvePath(
367
+ dest,
368
+ resolveBase
369
+ )}`
363
370
  };
364
371
  }
365
372
  return this.checkProtectedPath(dest, 'Command "cp"', resolveBase);
@@ -371,7 +378,10 @@ var CommandAnalyzer = class {
371
378
  if (!this.isPathAllowed(dest, true, resolveBase)) {
372
379
  return {
373
380
  blocked: true,
374
- reason: `Command "dd" targets path outside working directory: ${this.resolvePath(dest, resolveBase)}`
381
+ reason: `Command "dd" targets path outside working directory: ${this.resolvePath(
382
+ dest,
383
+ resolveBase
384
+ )}`
375
385
  };
376
386
  }
377
387
  return this.checkProtectedPath(dest, 'Command "dd"', resolveBase);
@@ -383,7 +393,10 @@ var CommandAnalyzer = class {
383
393
  if (!this.isPathAllowed(dest, true, resolveBase)) {
384
394
  return {
385
395
  blocked: true,
386
- reason: `Command "mv" targets path outside working directory: ${this.resolvePath(dest, resolveBase)}`
396
+ reason: `Command "mv" targets path outside working directory: ${this.resolvePath(
397
+ dest,
398
+ resolveBase
399
+ )}`
387
400
  };
388
401
  }
389
402
  const destResult = this.checkProtectedPath(
@@ -396,7 +409,10 @@ var CommandAnalyzer = class {
396
409
  if (!this.isPathAllowed(source, false, resolveBase)) {
397
410
  return {
398
411
  blocked: true,
399
- reason: `Command "mv" targets path outside working directory: ${this.resolvePath(source, resolveBase)}`
412
+ reason: `Command "mv" targets path outside working directory: ${this.resolvePath(
413
+ source,
414
+ resolveBase
415
+ )}`
400
416
  };
401
417
  }
402
418
  const sourceResult = this.checkProtectedPath(
@@ -413,7 +429,10 @@ var CommandAnalyzer = class {
413
429
  if (!this.isPathAllowed(path, false, resolveBase)) {
414
430
  return {
415
431
  blocked: true,
416
- reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(path, resolveBase)}`
432
+ reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(
433
+ path,
434
+ resolveBase
435
+ )}`
417
436
  };
418
437
  }
419
438
  const result = this.checkProtectedPath(
@@ -431,7 +450,10 @@ var CommandAnalyzer = class {
431
450
  if (!this.isPathAllowed(path, allowDevicePaths, resolveBase)) {
432
451
  return {
433
452
  blocked: true,
434
- reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(path, resolveBase)}`
453
+ reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(
454
+ path,
455
+ resolveBase
456
+ )}`
435
457
  };
436
458
  }
437
459
  const result = this.checkProtectedPath(
@@ -347,7 +347,11 @@ var CommandAnalyzer = class {
347
347
  reason: `Command "${name}" targets path outside working directory: ${path}`
348
348
  };
349
349
  }
350
- const result = this.checkProtectedPath(path, `Command "${name}"`, resolveBase);
350
+ const result = this.checkProtectedPath(
351
+ path,
352
+ `Command "${name}"`,
353
+ resolveBase
354
+ );
351
355
  if (result.blocked) return result;
352
356
  }
353
357
  }
@@ -359,7 +363,10 @@ var CommandAnalyzer = class {
359
363
  if (!this.isPathAllowed(dest, true, resolveBase)) {
360
364
  return {
361
365
  blocked: true,
362
- reason: `Command "cp" targets path outside working directory: ${this.resolvePath(dest, resolveBase)}`
366
+ reason: `Command "cp" targets path outside working directory: ${this.resolvePath(
367
+ dest,
368
+ resolveBase
369
+ )}`
363
370
  };
364
371
  }
365
372
  return this.checkProtectedPath(dest, 'Command "cp"', resolveBase);
@@ -371,7 +378,10 @@ var CommandAnalyzer = class {
371
378
  if (!this.isPathAllowed(dest, true, resolveBase)) {
372
379
  return {
373
380
  blocked: true,
374
- reason: `Command "dd" targets path outside working directory: ${this.resolvePath(dest, resolveBase)}`
381
+ reason: `Command "dd" targets path outside working directory: ${this.resolvePath(
382
+ dest,
383
+ resolveBase
384
+ )}`
375
385
  };
376
386
  }
377
387
  return this.checkProtectedPath(dest, 'Command "dd"', resolveBase);
@@ -383,7 +393,10 @@ var CommandAnalyzer = class {
383
393
  if (!this.isPathAllowed(dest, true, resolveBase)) {
384
394
  return {
385
395
  blocked: true,
386
- reason: `Command "mv" targets path outside working directory: ${this.resolvePath(dest, resolveBase)}`
396
+ reason: `Command "mv" targets path outside working directory: ${this.resolvePath(
397
+ dest,
398
+ resolveBase
399
+ )}`
387
400
  };
388
401
  }
389
402
  const destResult = this.checkProtectedPath(
@@ -396,7 +409,10 @@ var CommandAnalyzer = class {
396
409
  if (!this.isPathAllowed(source, false, resolveBase)) {
397
410
  return {
398
411
  blocked: true,
399
- reason: `Command "mv" targets path outside working directory: ${this.resolvePath(source, resolveBase)}`
412
+ reason: `Command "mv" targets path outside working directory: ${this.resolvePath(
413
+ source,
414
+ resolveBase
415
+ )}`
400
416
  };
401
417
  }
402
418
  const sourceResult = this.checkProtectedPath(
@@ -413,7 +429,10 @@ var CommandAnalyzer = class {
413
429
  if (!this.isPathAllowed(path, false, resolveBase)) {
414
430
  return {
415
431
  blocked: true,
416
- reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(path, resolveBase)}`
432
+ reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(
433
+ path,
434
+ resolveBase
435
+ )}`
417
436
  };
418
437
  }
419
438
  const result = this.checkProtectedPath(
@@ -431,7 +450,10 @@ var CommandAnalyzer = class {
431
450
  if (!this.isPathAllowed(path, allowDevicePaths, resolveBase)) {
432
451
  return {
433
452
  blocked: true,
434
- reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(path, resolveBase)}`
453
+ reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(
454
+ path,
455
+ resolveBase
456
+ )}`
435
457
  };
436
458
  }
437
459
  const result = this.checkProtectedPath(
@@ -345,7 +345,11 @@ var CommandAnalyzer = class {
345
345
  reason: `Command "${name}" targets path outside working directory: ${path}`
346
346
  };
347
347
  }
348
- const result = this.checkProtectedPath(path, `Command "${name}"`, resolveBase);
348
+ const result = this.checkProtectedPath(
349
+ path,
350
+ `Command "${name}"`,
351
+ resolveBase
352
+ );
349
353
  if (result.blocked) return result;
350
354
  }
351
355
  }
@@ -357,7 +361,10 @@ var CommandAnalyzer = class {
357
361
  if (!this.isPathAllowed(dest, true, resolveBase)) {
358
362
  return {
359
363
  blocked: true,
360
- reason: `Command "cp" targets path outside working directory: ${this.resolvePath(dest, resolveBase)}`
364
+ reason: `Command "cp" targets path outside working directory: ${this.resolvePath(
365
+ dest,
366
+ resolveBase
367
+ )}`
361
368
  };
362
369
  }
363
370
  return this.checkProtectedPath(dest, 'Command "cp"', resolveBase);
@@ -369,7 +376,10 @@ var CommandAnalyzer = class {
369
376
  if (!this.isPathAllowed(dest, true, resolveBase)) {
370
377
  return {
371
378
  blocked: true,
372
- reason: `Command "dd" targets path outside working directory: ${this.resolvePath(dest, resolveBase)}`
379
+ reason: `Command "dd" targets path outside working directory: ${this.resolvePath(
380
+ dest,
381
+ resolveBase
382
+ )}`
373
383
  };
374
384
  }
375
385
  return this.checkProtectedPath(dest, 'Command "dd"', resolveBase);
@@ -381,7 +391,10 @@ var CommandAnalyzer = class {
381
391
  if (!this.isPathAllowed(dest, true, resolveBase)) {
382
392
  return {
383
393
  blocked: true,
384
- reason: `Command "mv" targets path outside working directory: ${this.resolvePath(dest, resolveBase)}`
394
+ reason: `Command "mv" targets path outside working directory: ${this.resolvePath(
395
+ dest,
396
+ resolveBase
397
+ )}`
385
398
  };
386
399
  }
387
400
  const destResult = this.checkProtectedPath(
@@ -394,7 +407,10 @@ var CommandAnalyzer = class {
394
407
  if (!this.isPathAllowed(source, false, resolveBase)) {
395
408
  return {
396
409
  blocked: true,
397
- reason: `Command "mv" targets path outside working directory: ${this.resolvePath(source, resolveBase)}`
410
+ reason: `Command "mv" targets path outside working directory: ${this.resolvePath(
411
+ source,
412
+ resolveBase
413
+ )}`
398
414
  };
399
415
  }
400
416
  const sourceResult = this.checkProtectedPath(
@@ -411,7 +427,10 @@ var CommandAnalyzer = class {
411
427
  if (!this.isPathAllowed(path, false, resolveBase)) {
412
428
  return {
413
429
  blocked: true,
414
- reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(path, resolveBase)}`
430
+ reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(
431
+ path,
432
+ resolveBase
433
+ )}`
415
434
  };
416
435
  }
417
436
  const result = this.checkProtectedPath(
@@ -429,7 +448,10 @@ var CommandAnalyzer = class {
429
448
  if (!this.isPathAllowed(path, allowDevicePaths, resolveBase)) {
430
449
  return {
431
450
  blocked: true,
432
- reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(path, resolveBase)}`
451
+ reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(
452
+ path,
453
+ resolveBase
454
+ )}`
433
455
  };
434
456
  }
435
457
  const result = this.checkProtectedPath(
package/dist/pi/leash.js CHANGED
@@ -345,7 +345,11 @@ var CommandAnalyzer = class {
345
345
  reason: `Command "${name}" targets path outside working directory: ${path}`
346
346
  };
347
347
  }
348
- const result = this.checkProtectedPath(path, `Command "${name}"`, resolveBase);
348
+ const result = this.checkProtectedPath(
349
+ path,
350
+ `Command "${name}"`,
351
+ resolveBase
352
+ );
349
353
  if (result.blocked) return result;
350
354
  }
351
355
  }
@@ -357,7 +361,10 @@ var CommandAnalyzer = class {
357
361
  if (!this.isPathAllowed(dest, true, resolveBase)) {
358
362
  return {
359
363
  blocked: true,
360
- reason: `Command "cp" targets path outside working directory: ${this.resolvePath(dest, resolveBase)}`
364
+ reason: `Command "cp" targets path outside working directory: ${this.resolvePath(
365
+ dest,
366
+ resolveBase
367
+ )}`
361
368
  };
362
369
  }
363
370
  return this.checkProtectedPath(dest, 'Command "cp"', resolveBase);
@@ -369,7 +376,10 @@ var CommandAnalyzer = class {
369
376
  if (!this.isPathAllowed(dest, true, resolveBase)) {
370
377
  return {
371
378
  blocked: true,
372
- reason: `Command "dd" targets path outside working directory: ${this.resolvePath(dest, resolveBase)}`
379
+ reason: `Command "dd" targets path outside working directory: ${this.resolvePath(
380
+ dest,
381
+ resolveBase
382
+ )}`
373
383
  };
374
384
  }
375
385
  return this.checkProtectedPath(dest, 'Command "dd"', resolveBase);
@@ -381,7 +391,10 @@ var CommandAnalyzer = class {
381
391
  if (!this.isPathAllowed(dest, true, resolveBase)) {
382
392
  return {
383
393
  blocked: true,
384
- reason: `Command "mv" targets path outside working directory: ${this.resolvePath(dest, resolveBase)}`
394
+ reason: `Command "mv" targets path outside working directory: ${this.resolvePath(
395
+ dest,
396
+ resolveBase
397
+ )}`
385
398
  };
386
399
  }
387
400
  const destResult = this.checkProtectedPath(
@@ -394,7 +407,10 @@ var CommandAnalyzer = class {
394
407
  if (!this.isPathAllowed(source, false, resolveBase)) {
395
408
  return {
396
409
  blocked: true,
397
- reason: `Command "mv" targets path outside working directory: ${this.resolvePath(source, resolveBase)}`
410
+ reason: `Command "mv" targets path outside working directory: ${this.resolvePath(
411
+ source,
412
+ resolveBase
413
+ )}`
398
414
  };
399
415
  }
400
416
  const sourceResult = this.checkProtectedPath(
@@ -411,7 +427,10 @@ var CommandAnalyzer = class {
411
427
  if (!this.isPathAllowed(path, false, resolveBase)) {
412
428
  return {
413
429
  blocked: true,
414
- reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(path, resolveBase)}`
430
+ reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(
431
+ path,
432
+ resolveBase
433
+ )}`
415
434
  };
416
435
  }
417
436
  const result = this.checkProtectedPath(
@@ -429,7 +448,10 @@ var CommandAnalyzer = class {
429
448
  if (!this.isPathAllowed(path, allowDevicePaths, resolveBase)) {
430
449
  return {
431
450
  blocked: true,
432
- reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(path, resolveBase)}`
451
+ reason: `Command "${baseCmd}" targets path outside working directory: ${this.resolvePath(
452
+ path,
453
+ resolveBase
454
+ )}`
433
455
  };
434
456
  }
435
457
  const result = this.checkProtectedPath(
@@ -544,17 +566,15 @@ async function checkForUpdates() {
544
566
  // packages/pi/leash.ts
545
567
  function leash_default(pi) {
546
568
  let analyzer = null;
547
- pi.on("session", async (event, ctx) => {
548
- if (event.reason === "start") {
549
- analyzer = new CommandAnalyzer(ctx.cwd);
550
- ctx.ui.notify("\u{1F512} Leash active", "info");
551
- const update = await checkForUpdates();
552
- if (update.hasUpdate) {
553
- ctx.ui.notify(
554
- `\u{1F504} Leash ${update.latestVersion} available. Run: leash --update (restart required)`,
555
- "warning"
556
- );
557
- }
569
+ pi.on("session_start", async (_event, ctx) => {
570
+ analyzer = new CommandAnalyzer(ctx.cwd);
571
+ ctx.ui.notify("\u{1F512} Leash active", "info");
572
+ const update = await checkForUpdates();
573
+ if (update.hasUpdate) {
574
+ ctx.ui.notify(
575
+ `\u{1F504} Leash ${update.latestVersion} available. Run: leash --update (restart required)`,
576
+ "warning"
577
+ );
558
578
  }
559
579
  });
560
580
  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.7",
3
+ "version": "1.0.9",
4
4
  "type": "module",
5
5
  "description": "Security guardrails for AI coding agents",
6
6
  "bin": {
@@ -49,8 +49,8 @@
49
49
  "build:factory": "esbuild packages/factory/leash.ts --bundle --outfile=dist/factory/leash.js --platform=node --format=esm"
50
50
  },
51
51
  "devDependencies": {
52
- "@mariozechner/pi-coding-agent": "^0.27.2",
53
- "@opencode-ai/plugin": "^1.0.191",
52
+ "@mariozechner/pi-coding-agent": "^0.36.0",
53
+ "@opencode-ai/plugin": "^1.0.224",
54
54
  "@types/node": "^22.19.3",
55
55
  "esbuild": "^0.27.2",
56
56
  "typescript": "^5.9.3"