@melihmucuk/leash 1.0.4 → 1.0.6
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 +20 -0
- package/dist/claude-code/leash.js +16 -3
- package/dist/factory/leash.js +16 -3
- package/dist/opencode/leash.js +16 -3
- package/dist/pi/leash.js +16 -3
- package/package.json +3 -2
- package/packages/core/lib/version-checker.js +44 -0
package/README.md
CHANGED
|
@@ -16,6 +16,16 @@ AI agents can hallucinate dangerous commands. Leash sandboxes them:
|
|
|
16
16
|
|
|
17
17
|

|
|
18
18
|
|
|
19
|
+
## Example horror stories
|
|
20
|
+
|
|
21
|
+
<img height="400" alt="image" src="https://github.com/user-attachments/assets/db503024-94ca-4443-b80e-b63fbc740367" />
|
|
22
|
+
|
|
23
|
+
<img height="400" alt="image" src="https://github.com/user-attachments/assets/94f0a4e5-db6c-4b14-bddd-b8984c51ed3d" />
|
|
24
|
+
|
|
25
|
+
Links:
|
|
26
|
+
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
|
+
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
|
+
|
|
19
29
|
## Quick Start
|
|
20
30
|
|
|
21
31
|
```bash
|
|
@@ -164,6 +174,8 @@ rm -rf /tmp/build-cache # ✅ Temp directory
|
|
|
164
174
|
rm .env.example # ✅ Example files allowed
|
|
165
175
|
git commit -m "message" # ✅ Safe git commands
|
|
166
176
|
git push origin main # ✅ Normal push (no --force)
|
|
177
|
+
echo "plan" > ~/.claude/plans/x # ✅ Platform config directories
|
|
178
|
+
rm ~/.pi/agent/old.md # ✅ Platform config directories
|
|
167
179
|
```
|
|
168
180
|
|
|
169
181
|
<details>
|
|
@@ -277,6 +289,14 @@ rm -rf /tmp/build-cache
|
|
|
277
289
|
echo "data" > /tmp/output.txt
|
|
278
290
|
rsync -av --delete ./src/ /tmp/backup/
|
|
279
291
|
|
|
292
|
+
# Platform config directories
|
|
293
|
+
rm ~/.claude/plans/old-plan.md
|
|
294
|
+
echo "config" > ~/.factory/cache.json
|
|
295
|
+
rm ~/.pi/agent/temp.md
|
|
296
|
+
rm ~/.config/opencode/cache.json
|
|
297
|
+
find ~/.claude -name '*.tmp' -delete
|
|
298
|
+
rsync -av --delete ./src/ ~/.pi/backup/
|
|
299
|
+
|
|
280
300
|
# Device paths
|
|
281
301
|
echo "x" > /dev/null
|
|
282
302
|
truncate -s 0 /dev/null
|
|
@@ -32,6 +32,12 @@ var DANGEROUS_PATTERNS = [
|
|
|
32
32
|
];
|
|
33
33
|
var REDIRECT_PATTERN = />{1,2}\s*(?:"([^"]+)"|'([^']+)'|([^\s;|&>]+))/g;
|
|
34
34
|
var DEVICE_PATHS = ["/dev/null", "/dev/stdin", "/dev/stdout", "/dev/stderr"];
|
|
35
|
+
var PLATFORM_PATHS = [
|
|
36
|
+
".claude",
|
|
37
|
+
".factory",
|
|
38
|
+
".pi",
|
|
39
|
+
".config/opencode"
|
|
40
|
+
];
|
|
35
41
|
var TEMP_PATHS = [
|
|
36
42
|
"/tmp",
|
|
37
43
|
"/var/tmp",
|
|
@@ -103,6 +109,12 @@ var PathValidator = class {
|
|
|
103
109
|
const resolved = this.resolveReal(path);
|
|
104
110
|
return this.matchesAny(resolved, TEMP_PATHS);
|
|
105
111
|
}
|
|
112
|
+
isPlatformPath(path) {
|
|
113
|
+
const resolved = this.resolveReal(path);
|
|
114
|
+
const home = homedir();
|
|
115
|
+
const platformPaths = PLATFORM_PATHS.map((p) => `${home}/${p}`);
|
|
116
|
+
return this.matchesAny(resolved, platformPaths);
|
|
117
|
+
}
|
|
106
118
|
isProtectedPath(path) {
|
|
107
119
|
if (!this.isWithinWorkingDir(path)) {
|
|
108
120
|
return { protected: false };
|
|
@@ -134,6 +146,7 @@ var CommandAnalyzer = class {
|
|
|
134
146
|
isPathAllowed(path, allowDevicePaths, resolveBase) {
|
|
135
147
|
const resolved = this.resolvePath(path, resolveBase);
|
|
136
148
|
if (this.pathValidator.isWithinWorkingDir(resolved)) return true;
|
|
149
|
+
if (this.pathValidator.isPlatformPath(resolved)) return true;
|
|
137
150
|
return allowDevicePaths ? this.pathValidator.isSafeForWrite(resolved) : this.pathValidator.isTempPath(resolved);
|
|
138
151
|
}
|
|
139
152
|
checkProtectedPath(path, context, resolveBase) {
|
|
@@ -262,7 +275,7 @@ var CommandAnalyzer = class {
|
|
|
262
275
|
if (!path || path.startsWith("&")) {
|
|
263
276
|
continue;
|
|
264
277
|
}
|
|
265
|
-
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
|
|
278
|
+
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
|
|
266
279
|
return {
|
|
267
280
|
blocked: true,
|
|
268
281
|
reason: `Redirect to path outside working directory: ${path}`
|
|
@@ -306,7 +319,7 @@ var CommandAnalyzer = class {
|
|
|
306
319
|
const paths = this.extractPaths(command);
|
|
307
320
|
for (const path of paths) {
|
|
308
321
|
const resolved = this.resolvePath(path, resolveBase);
|
|
309
|
-
if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved)) {
|
|
322
|
+
if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved) && !this.pathValidator.isPlatformPath(resolved)) {
|
|
310
323
|
return {
|
|
311
324
|
blocked: true,
|
|
312
325
|
reason: `Command "${name}" targets path outside working directory: ${path}`
|
|
@@ -456,7 +469,7 @@ var CommandAnalyzer = class {
|
|
|
456
469
|
}
|
|
457
470
|
validatePath(path) {
|
|
458
471
|
if (!path) return { blocked: false };
|
|
459
|
-
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
|
|
472
|
+
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
|
|
460
473
|
return {
|
|
461
474
|
blocked: true,
|
|
462
475
|
reason: `File operation targets path outside working directory: ${path}`
|
package/dist/factory/leash.js
CHANGED
|
@@ -32,6 +32,12 @@ var DANGEROUS_PATTERNS = [
|
|
|
32
32
|
];
|
|
33
33
|
var REDIRECT_PATTERN = />{1,2}\s*(?:"([^"]+)"|'([^']+)'|([^\s;|&>]+))/g;
|
|
34
34
|
var DEVICE_PATHS = ["/dev/null", "/dev/stdin", "/dev/stdout", "/dev/stderr"];
|
|
35
|
+
var PLATFORM_PATHS = [
|
|
36
|
+
".claude",
|
|
37
|
+
".factory",
|
|
38
|
+
".pi",
|
|
39
|
+
".config/opencode"
|
|
40
|
+
];
|
|
35
41
|
var TEMP_PATHS = [
|
|
36
42
|
"/tmp",
|
|
37
43
|
"/var/tmp",
|
|
@@ -103,6 +109,12 @@ var PathValidator = class {
|
|
|
103
109
|
const resolved = this.resolveReal(path);
|
|
104
110
|
return this.matchesAny(resolved, TEMP_PATHS);
|
|
105
111
|
}
|
|
112
|
+
isPlatformPath(path) {
|
|
113
|
+
const resolved = this.resolveReal(path);
|
|
114
|
+
const home = homedir();
|
|
115
|
+
const platformPaths = PLATFORM_PATHS.map((p) => `${home}/${p}`);
|
|
116
|
+
return this.matchesAny(resolved, platformPaths);
|
|
117
|
+
}
|
|
106
118
|
isProtectedPath(path) {
|
|
107
119
|
if (!this.isWithinWorkingDir(path)) {
|
|
108
120
|
return { protected: false };
|
|
@@ -134,6 +146,7 @@ var CommandAnalyzer = class {
|
|
|
134
146
|
isPathAllowed(path, allowDevicePaths, resolveBase) {
|
|
135
147
|
const resolved = this.resolvePath(path, resolveBase);
|
|
136
148
|
if (this.pathValidator.isWithinWorkingDir(resolved)) return true;
|
|
149
|
+
if (this.pathValidator.isPlatformPath(resolved)) return true;
|
|
137
150
|
return allowDevicePaths ? this.pathValidator.isSafeForWrite(resolved) : this.pathValidator.isTempPath(resolved);
|
|
138
151
|
}
|
|
139
152
|
checkProtectedPath(path, context, resolveBase) {
|
|
@@ -262,7 +275,7 @@ var CommandAnalyzer = class {
|
|
|
262
275
|
if (!path || path.startsWith("&")) {
|
|
263
276
|
continue;
|
|
264
277
|
}
|
|
265
|
-
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
|
|
278
|
+
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
|
|
266
279
|
return {
|
|
267
280
|
blocked: true,
|
|
268
281
|
reason: `Redirect to path outside working directory: ${path}`
|
|
@@ -306,7 +319,7 @@ var CommandAnalyzer = class {
|
|
|
306
319
|
const paths = this.extractPaths(command);
|
|
307
320
|
for (const path of paths) {
|
|
308
321
|
const resolved = this.resolvePath(path, resolveBase);
|
|
309
|
-
if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved)) {
|
|
322
|
+
if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved) && !this.pathValidator.isPlatformPath(resolved)) {
|
|
310
323
|
return {
|
|
311
324
|
blocked: true,
|
|
312
325
|
reason: `Command "${name}" targets path outside working directory: ${path}`
|
|
@@ -456,7 +469,7 @@ var CommandAnalyzer = class {
|
|
|
456
469
|
}
|
|
457
470
|
validatePath(path) {
|
|
458
471
|
if (!path) return { blocked: false };
|
|
459
|
-
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
|
|
472
|
+
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
|
|
460
473
|
return {
|
|
461
474
|
blocked: true,
|
|
462
475
|
reason: `File operation targets path outside working directory: ${path}`
|
package/dist/opencode/leash.js
CHANGED
|
@@ -30,6 +30,12 @@ var DANGEROUS_PATTERNS = [
|
|
|
30
30
|
];
|
|
31
31
|
var REDIRECT_PATTERN = />{1,2}\s*(?:"([^"]+)"|'([^']+)'|([^\s;|&>]+))/g;
|
|
32
32
|
var DEVICE_PATHS = ["/dev/null", "/dev/stdin", "/dev/stdout", "/dev/stderr"];
|
|
33
|
+
var PLATFORM_PATHS = [
|
|
34
|
+
".claude",
|
|
35
|
+
".factory",
|
|
36
|
+
".pi",
|
|
37
|
+
".config/opencode"
|
|
38
|
+
];
|
|
33
39
|
var TEMP_PATHS = [
|
|
34
40
|
"/tmp",
|
|
35
41
|
"/var/tmp",
|
|
@@ -101,6 +107,12 @@ var PathValidator = class {
|
|
|
101
107
|
const resolved = this.resolveReal(path);
|
|
102
108
|
return this.matchesAny(resolved, TEMP_PATHS);
|
|
103
109
|
}
|
|
110
|
+
isPlatformPath(path) {
|
|
111
|
+
const resolved = this.resolveReal(path);
|
|
112
|
+
const home = homedir();
|
|
113
|
+
const platformPaths = PLATFORM_PATHS.map((p) => `${home}/${p}`);
|
|
114
|
+
return this.matchesAny(resolved, platformPaths);
|
|
115
|
+
}
|
|
104
116
|
isProtectedPath(path) {
|
|
105
117
|
if (!this.isWithinWorkingDir(path)) {
|
|
106
118
|
return { protected: false };
|
|
@@ -132,6 +144,7 @@ var CommandAnalyzer = class {
|
|
|
132
144
|
isPathAllowed(path, allowDevicePaths, resolveBase) {
|
|
133
145
|
const resolved = this.resolvePath(path, resolveBase);
|
|
134
146
|
if (this.pathValidator.isWithinWorkingDir(resolved)) return true;
|
|
147
|
+
if (this.pathValidator.isPlatformPath(resolved)) return true;
|
|
135
148
|
return allowDevicePaths ? this.pathValidator.isSafeForWrite(resolved) : this.pathValidator.isTempPath(resolved);
|
|
136
149
|
}
|
|
137
150
|
checkProtectedPath(path, context, resolveBase) {
|
|
@@ -260,7 +273,7 @@ var CommandAnalyzer = class {
|
|
|
260
273
|
if (!path || path.startsWith("&")) {
|
|
261
274
|
continue;
|
|
262
275
|
}
|
|
263
|
-
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
|
|
276
|
+
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
|
|
264
277
|
return {
|
|
265
278
|
blocked: true,
|
|
266
279
|
reason: `Redirect to path outside working directory: ${path}`
|
|
@@ -304,7 +317,7 @@ var CommandAnalyzer = class {
|
|
|
304
317
|
const paths = this.extractPaths(command);
|
|
305
318
|
for (const path of paths) {
|
|
306
319
|
const resolved = this.resolvePath(path, resolveBase);
|
|
307
|
-
if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved)) {
|
|
320
|
+
if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved) && !this.pathValidator.isPlatformPath(resolved)) {
|
|
308
321
|
return {
|
|
309
322
|
blocked: true,
|
|
310
323
|
reason: `Command "${name}" targets path outside working directory: ${path}`
|
|
@@ -454,7 +467,7 @@ var CommandAnalyzer = class {
|
|
|
454
467
|
}
|
|
455
468
|
validatePath(path) {
|
|
456
469
|
if (!path) return { blocked: false };
|
|
457
|
-
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
|
|
470
|
+
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
|
|
458
471
|
return {
|
|
459
472
|
blocked: true,
|
|
460
473
|
reason: `File operation targets path outside working directory: ${path}`
|
package/dist/pi/leash.js
CHANGED
|
@@ -30,6 +30,12 @@ var DANGEROUS_PATTERNS = [
|
|
|
30
30
|
];
|
|
31
31
|
var REDIRECT_PATTERN = />{1,2}\s*(?:"([^"]+)"|'([^']+)'|([^\s;|&>]+))/g;
|
|
32
32
|
var DEVICE_PATHS = ["/dev/null", "/dev/stdin", "/dev/stdout", "/dev/stderr"];
|
|
33
|
+
var PLATFORM_PATHS = [
|
|
34
|
+
".claude",
|
|
35
|
+
".factory",
|
|
36
|
+
".pi",
|
|
37
|
+
".config/opencode"
|
|
38
|
+
];
|
|
33
39
|
var TEMP_PATHS = [
|
|
34
40
|
"/tmp",
|
|
35
41
|
"/var/tmp",
|
|
@@ -101,6 +107,12 @@ var PathValidator = class {
|
|
|
101
107
|
const resolved = this.resolveReal(path);
|
|
102
108
|
return this.matchesAny(resolved, TEMP_PATHS);
|
|
103
109
|
}
|
|
110
|
+
isPlatformPath(path) {
|
|
111
|
+
const resolved = this.resolveReal(path);
|
|
112
|
+
const home = homedir();
|
|
113
|
+
const platformPaths = PLATFORM_PATHS.map((p) => `${home}/${p}`);
|
|
114
|
+
return this.matchesAny(resolved, platformPaths);
|
|
115
|
+
}
|
|
104
116
|
isProtectedPath(path) {
|
|
105
117
|
if (!this.isWithinWorkingDir(path)) {
|
|
106
118
|
return { protected: false };
|
|
@@ -132,6 +144,7 @@ var CommandAnalyzer = class {
|
|
|
132
144
|
isPathAllowed(path, allowDevicePaths, resolveBase) {
|
|
133
145
|
const resolved = this.resolvePath(path, resolveBase);
|
|
134
146
|
if (this.pathValidator.isWithinWorkingDir(resolved)) return true;
|
|
147
|
+
if (this.pathValidator.isPlatformPath(resolved)) return true;
|
|
135
148
|
return allowDevicePaths ? this.pathValidator.isSafeForWrite(resolved) : this.pathValidator.isTempPath(resolved);
|
|
136
149
|
}
|
|
137
150
|
checkProtectedPath(path, context, resolveBase) {
|
|
@@ -260,7 +273,7 @@ var CommandAnalyzer = class {
|
|
|
260
273
|
if (!path || path.startsWith("&")) {
|
|
261
274
|
continue;
|
|
262
275
|
}
|
|
263
|
-
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
|
|
276
|
+
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
|
|
264
277
|
return {
|
|
265
278
|
blocked: true,
|
|
266
279
|
reason: `Redirect to path outside working directory: ${path}`
|
|
@@ -304,7 +317,7 @@ var CommandAnalyzer = class {
|
|
|
304
317
|
const paths = this.extractPaths(command);
|
|
305
318
|
for (const path of paths) {
|
|
306
319
|
const resolved = this.resolvePath(path, resolveBase);
|
|
307
|
-
if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved)) {
|
|
320
|
+
if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved) && !this.pathValidator.isPlatformPath(resolved)) {
|
|
308
321
|
return {
|
|
309
322
|
blocked: true,
|
|
310
323
|
reason: `Command "${name}" targets path outside working directory: ${path}`
|
|
@@ -454,7 +467,7 @@ var CommandAnalyzer = class {
|
|
|
454
467
|
}
|
|
455
468
|
validatePath(path) {
|
|
456
469
|
if (!path) return { blocked: false };
|
|
457
|
-
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
|
|
470
|
+
if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
|
|
458
471
|
return {
|
|
459
472
|
blocked: true,
|
|
460
473
|
reason: `File operation targets path outside working directory: ${path}`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@melihmucuk/leash",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Security guardrails for AI coding agents",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/",
|
|
11
11
|
"bin/",
|
|
12
|
-
"!bin/test/"
|
|
12
|
+
"!bin/test/",
|
|
13
|
+
"packages/core/lib/version-checker.js"
|
|
13
14
|
],
|
|
14
15
|
"author": "Melih Mucuk",
|
|
15
16
|
"license": "MIT",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
function getVersion() {
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const candidates = [
|
|
7
|
+
join(__dirname, "..", "..", "package.json"),
|
|
8
|
+
join(__dirname, "..", "..", "..", "package.json")
|
|
9
|
+
];
|
|
10
|
+
for (const path of candidates) {
|
|
11
|
+
if (existsSync(path)) {
|
|
12
|
+
try {
|
|
13
|
+
const pkg = JSON.parse(readFileSync(path, "utf-8"));
|
|
14
|
+
if (pkg.name === "@melihmucuk/leash") {
|
|
15
|
+
return pkg.version;
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return "0.0.0";
|
|
22
|
+
}
|
|
23
|
+
const CURRENT_VERSION = getVersion();
|
|
24
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org/@melihmucuk/leash/latest";
|
|
25
|
+
async function checkForUpdates() {
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(NPM_REGISTRY_URL);
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
return { hasUpdate: false, currentVersion: CURRENT_VERSION };
|
|
30
|
+
}
|
|
31
|
+
const data = await response.json();
|
|
32
|
+
return {
|
|
33
|
+
hasUpdate: data.version !== CURRENT_VERSION,
|
|
34
|
+
latestVersion: data.version,
|
|
35
|
+
currentVersion: CURRENT_VERSION
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
return { hasUpdate: false, currentVersion: CURRENT_VERSION };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
CURRENT_VERSION,
|
|
43
|
+
checkForUpdates
|
|
44
|
+
};
|