@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 +20 -0
- package/dist/claude-code/leash.js +39 -4
- package/dist/factory/leash.js +39 -4
- package/dist/opencode/leash.js +39 -4
- package/dist/pi/leash.js +39 -4
- package/package.json +1 -1
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 };
|
|
@@ -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
|
|
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}`
|
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 };
|
|
@@ -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
|
|
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}`
|
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 };
|
|
@@ -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
|
|
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
|
|
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}`
|