@melihmucuk/leash 1.0.5 → 1.0.7

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
@@ -16,6 +16,16 @@ AI agents can hallucinate dangerous commands. Leash sandboxes them:
16
16
 
17
17
  ![Claude Code](assets/claude-code.png)
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 };
@@ -131,9 +143,31 @@ var CommandAnalyzer = class {
131
143
  const expanded = this.pathValidator.expand(path);
132
144
  return resolveBase ? resolve2(resolveBase, expanded) : expanded;
133
145
  }
146
+ /**
147
+ * Strip heredoc content from command before analyzing redirects.
148
+ * Handles: <<EOF, <<'EOF', <<"EOF", <<-EOF
149
+ */
150
+ stripHeredocs(command) {
151
+ const heredocStart = /<<-?\s*(['"]?)(\w+)\1/g;
152
+ let result = command;
153
+ let match;
154
+ while ((match = heredocStart.exec(command)) !== null) {
155
+ const delimiter = match[2];
156
+ const endPattern = new RegExp(`\\n\\t*${delimiter}\\s*(?:\\n|$)`);
157
+ const startIndex = match.index;
158
+ const contentAfterStart = command.slice(match.index + match[0].length);
159
+ const endMatch = endPattern.exec(contentAfterStart);
160
+ if (endMatch) {
161
+ const endIndex = match.index + match[0].length + endMatch.index + endMatch[0].length;
162
+ result = result.slice(0, startIndex) + result.slice(endIndex);
163
+ }
164
+ }
165
+ return result;
166
+ }
134
167
  isPathAllowed(path, allowDevicePaths, resolveBase) {
135
168
  const resolved = this.resolvePath(path, resolveBase);
136
169
  if (this.pathValidator.isWithinWorkingDir(resolved)) return true;
170
+ if (this.pathValidator.isPlatformPath(resolved)) return true;
137
171
  return allowDevicePaths ? this.pathValidator.isSafeForWrite(resolved) : this.pathValidator.isTempPath(resolved);
138
172
  }
139
173
  checkProtectedPath(path, context, resolveBase) {
@@ -256,13 +290,14 @@ var CommandAnalyzer = class {
256
290
  return commands;
257
291
  }
258
292
  checkRedirects(command) {
259
- const matches = command.matchAll(REDIRECT_PATTERN);
293
+ const strippedCommand = this.stripHeredocs(command);
294
+ const matches = strippedCommand.matchAll(REDIRECT_PATTERN);
260
295
  for (const match of matches) {
261
296
  const path = match[1] || match[2] || match[3];
262
297
  if (!path || path.startsWith("&")) {
263
298
  continue;
264
299
  }
265
- if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
300
+ if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
266
301
  return {
267
302
  blocked: true,
268
303
  reason: `Redirect to path outside working directory: ${path}`
@@ -306,7 +341,7 @@ var CommandAnalyzer = class {
306
341
  const paths = this.extractPaths(command);
307
342
  for (const path of paths) {
308
343
  const resolved = this.resolvePath(path, resolveBase);
309
- if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved)) {
344
+ if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved) && !this.pathValidator.isPlatformPath(resolved)) {
310
345
  return {
311
346
  blocked: true,
312
347
  reason: `Command "${name}" targets path outside working directory: ${path}`
@@ -456,7 +491,7 @@ var CommandAnalyzer = class {
456
491
  }
457
492
  validatePath(path) {
458
493
  if (!path) return { blocked: false };
459
- if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
494
+ if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
460
495
  return {
461
496
  blocked: true,
462
497
  reason: `File operation targets path outside working directory: ${path}`
@@ -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 };
@@ -131,9 +143,31 @@ var CommandAnalyzer = class {
131
143
  const expanded = this.pathValidator.expand(path);
132
144
  return resolveBase ? resolve2(resolveBase, expanded) : expanded;
133
145
  }
146
+ /**
147
+ * Strip heredoc content from command before analyzing redirects.
148
+ * Handles: <<EOF, <<'EOF', <<"EOF", <<-EOF
149
+ */
150
+ stripHeredocs(command) {
151
+ const heredocStart = /<<-?\s*(['"]?)(\w+)\1/g;
152
+ let result = command;
153
+ let match;
154
+ while ((match = heredocStart.exec(command)) !== null) {
155
+ const delimiter = match[2];
156
+ const endPattern = new RegExp(`\\n\\t*${delimiter}\\s*(?:\\n|$)`);
157
+ const startIndex = match.index;
158
+ const contentAfterStart = command.slice(match.index + match[0].length);
159
+ const endMatch = endPattern.exec(contentAfterStart);
160
+ if (endMatch) {
161
+ const endIndex = match.index + match[0].length + endMatch.index + endMatch[0].length;
162
+ result = result.slice(0, startIndex) + result.slice(endIndex);
163
+ }
164
+ }
165
+ return result;
166
+ }
134
167
  isPathAllowed(path, allowDevicePaths, resolveBase) {
135
168
  const resolved = this.resolvePath(path, resolveBase);
136
169
  if (this.pathValidator.isWithinWorkingDir(resolved)) return true;
170
+ if (this.pathValidator.isPlatformPath(resolved)) return true;
137
171
  return allowDevicePaths ? this.pathValidator.isSafeForWrite(resolved) : this.pathValidator.isTempPath(resolved);
138
172
  }
139
173
  checkProtectedPath(path, context, resolveBase) {
@@ -256,13 +290,14 @@ var CommandAnalyzer = class {
256
290
  return commands;
257
291
  }
258
292
  checkRedirects(command) {
259
- const matches = command.matchAll(REDIRECT_PATTERN);
293
+ const strippedCommand = this.stripHeredocs(command);
294
+ const matches = strippedCommand.matchAll(REDIRECT_PATTERN);
260
295
  for (const match of matches) {
261
296
  const path = match[1] || match[2] || match[3];
262
297
  if (!path || path.startsWith("&")) {
263
298
  continue;
264
299
  }
265
- if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
300
+ if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
266
301
  return {
267
302
  blocked: true,
268
303
  reason: `Redirect to path outside working directory: ${path}`
@@ -306,7 +341,7 @@ var CommandAnalyzer = class {
306
341
  const paths = this.extractPaths(command);
307
342
  for (const path of paths) {
308
343
  const resolved = this.resolvePath(path, resolveBase);
309
- if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved)) {
344
+ if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved) && !this.pathValidator.isPlatformPath(resolved)) {
310
345
  return {
311
346
  blocked: true,
312
347
  reason: `Command "${name}" targets path outside working directory: ${path}`
@@ -456,7 +491,7 @@ var CommandAnalyzer = class {
456
491
  }
457
492
  validatePath(path) {
458
493
  if (!path) return { blocked: false };
459
- if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
494
+ if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
460
495
  return {
461
496
  blocked: true,
462
497
  reason: `File operation targets path outside working directory: ${path}`
@@ -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 };
@@ -129,9 +141,31 @@ var CommandAnalyzer = class {
129
141
  const expanded = this.pathValidator.expand(path);
130
142
  return resolveBase ? resolve2(resolveBase, expanded) : expanded;
131
143
  }
144
+ /**
145
+ * Strip heredoc content from command before analyzing redirects.
146
+ * Handles: <<EOF, <<'EOF', <<"EOF", <<-EOF
147
+ */
148
+ stripHeredocs(command) {
149
+ const heredocStart = /<<-?\s*(['"]?)(\w+)\1/g;
150
+ let result = command;
151
+ let match;
152
+ while ((match = heredocStart.exec(command)) !== null) {
153
+ const delimiter = match[2];
154
+ const endPattern = new RegExp(`\\n\\t*${delimiter}\\s*(?:\\n|$)`);
155
+ const startIndex = match.index;
156
+ const contentAfterStart = command.slice(match.index + match[0].length);
157
+ const endMatch = endPattern.exec(contentAfterStart);
158
+ if (endMatch) {
159
+ const endIndex = match.index + match[0].length + endMatch.index + endMatch[0].length;
160
+ result = result.slice(0, startIndex) + result.slice(endIndex);
161
+ }
162
+ }
163
+ return result;
164
+ }
132
165
  isPathAllowed(path, allowDevicePaths, resolveBase) {
133
166
  const resolved = this.resolvePath(path, resolveBase);
134
167
  if (this.pathValidator.isWithinWorkingDir(resolved)) return true;
168
+ if (this.pathValidator.isPlatformPath(resolved)) return true;
135
169
  return allowDevicePaths ? this.pathValidator.isSafeForWrite(resolved) : this.pathValidator.isTempPath(resolved);
136
170
  }
137
171
  checkProtectedPath(path, context, resolveBase) {
@@ -254,13 +288,14 @@ var CommandAnalyzer = class {
254
288
  return commands;
255
289
  }
256
290
  checkRedirects(command) {
257
- const matches = command.matchAll(REDIRECT_PATTERN);
291
+ const strippedCommand = this.stripHeredocs(command);
292
+ const matches = strippedCommand.matchAll(REDIRECT_PATTERN);
258
293
  for (const match of matches) {
259
294
  const path = match[1] || match[2] || match[3];
260
295
  if (!path || path.startsWith("&")) {
261
296
  continue;
262
297
  }
263
- if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
298
+ if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
264
299
  return {
265
300
  blocked: true,
266
301
  reason: `Redirect to path outside working directory: ${path}`
@@ -304,7 +339,7 @@ var CommandAnalyzer = class {
304
339
  const paths = this.extractPaths(command);
305
340
  for (const path of paths) {
306
341
  const resolved = this.resolvePath(path, resolveBase);
307
- if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved)) {
342
+ if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved) && !this.pathValidator.isPlatformPath(resolved)) {
308
343
  return {
309
344
  blocked: true,
310
345
  reason: `Command "${name}" targets path outside working directory: ${path}`
@@ -454,7 +489,7 @@ var CommandAnalyzer = class {
454
489
  }
455
490
  validatePath(path) {
456
491
  if (!path) return { blocked: false };
457
- if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
492
+ if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
458
493
  return {
459
494
  blocked: true,
460
495
  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 };
@@ -129,9 +141,31 @@ var CommandAnalyzer = class {
129
141
  const expanded = this.pathValidator.expand(path);
130
142
  return resolveBase ? resolve2(resolveBase, expanded) : expanded;
131
143
  }
144
+ /**
145
+ * Strip heredoc content from command before analyzing redirects.
146
+ * Handles: <<EOF, <<'EOF', <<"EOF", <<-EOF
147
+ */
148
+ stripHeredocs(command) {
149
+ const heredocStart = /<<-?\s*(['"]?)(\w+)\1/g;
150
+ let result = command;
151
+ let match;
152
+ while ((match = heredocStart.exec(command)) !== null) {
153
+ const delimiter = match[2];
154
+ const endPattern = new RegExp(`\\n\\t*${delimiter}\\s*(?:\\n|$)`);
155
+ const startIndex = match.index;
156
+ const contentAfterStart = command.slice(match.index + match[0].length);
157
+ const endMatch = endPattern.exec(contentAfterStart);
158
+ if (endMatch) {
159
+ const endIndex = match.index + match[0].length + endMatch.index + endMatch[0].length;
160
+ result = result.slice(0, startIndex) + result.slice(endIndex);
161
+ }
162
+ }
163
+ return result;
164
+ }
132
165
  isPathAllowed(path, allowDevicePaths, resolveBase) {
133
166
  const resolved = this.resolvePath(path, resolveBase);
134
167
  if (this.pathValidator.isWithinWorkingDir(resolved)) return true;
168
+ if (this.pathValidator.isPlatformPath(resolved)) return true;
135
169
  return allowDevicePaths ? this.pathValidator.isSafeForWrite(resolved) : this.pathValidator.isTempPath(resolved);
136
170
  }
137
171
  checkProtectedPath(path, context, resolveBase) {
@@ -254,13 +288,14 @@ var CommandAnalyzer = class {
254
288
  return commands;
255
289
  }
256
290
  checkRedirects(command) {
257
- const matches = command.matchAll(REDIRECT_PATTERN);
291
+ const strippedCommand = this.stripHeredocs(command);
292
+ const matches = strippedCommand.matchAll(REDIRECT_PATTERN);
258
293
  for (const match of matches) {
259
294
  const path = match[1] || match[2] || match[3];
260
295
  if (!path || path.startsWith("&")) {
261
296
  continue;
262
297
  }
263
- if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
298
+ if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
264
299
  return {
265
300
  blocked: true,
266
301
  reason: `Redirect to path outside working directory: ${path}`
@@ -304,7 +339,7 @@ var CommandAnalyzer = class {
304
339
  const paths = this.extractPaths(command);
305
340
  for (const path of paths) {
306
341
  const resolved = this.resolvePath(path, resolveBase);
307
- if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved)) {
342
+ if (!this.pathValidator.isWithinWorkingDir(resolved) && !this.pathValidator.isTempPath(resolved) && !this.pathValidator.isPlatformPath(resolved)) {
308
343
  return {
309
344
  blocked: true,
310
345
  reason: `Command "${name}" targets path outside working directory: ${path}`
@@ -454,7 +489,7 @@ var CommandAnalyzer = class {
454
489
  }
455
490
  validatePath(path) {
456
491
  if (!path) return { blocked: false };
457
- if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path)) {
492
+ if (!this.pathValidator.isSafeForWrite(path) && !this.pathValidator.isWithinWorkingDir(path) && !this.pathValidator.isPlatformPath(path)) {
458
493
  return {
459
494
  blocked: true,
460
495
  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.5",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "description": "Security guardrails for AI coding agents",
6
6
  "bin": {