@melihmucuk/leash 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Melih Mucuk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,322 @@
1
+ # Leash 🔒
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@melihmucuk/leash.svg)](https://www.npmjs.com/package/@melihmucuk/leash)
4
+
5
+ **Security guardrails for AI coding agents.** Sandboxes file system access, blocks dangerous commands outside project directory, prevents destructive git operations, catches agent hallucinations before they cause damage.
6
+
7
+ ## Why Leash?
8
+
9
+ AI agents can hallucinate dangerous commands. Leash sandboxes them:
10
+
11
+ - Blocks `rm`, `mv`, `cp`, `chmod` outside working directory
12
+ - Protects sensitive files (`.env`, `.git`) even inside project
13
+ - Blocks `git reset --hard`, `push --force`, `clean -f`
14
+ - Resolves symlinks to prevent directory escapes
15
+ - Analyzes command chains (`&&`, `||`, `;`, `|`)
16
+
17
+ ![Claude Code](assets/claude-code.png)
18
+
19
+ ## Quick Start
20
+
21
+ ```bash
22
+ npm install -g @melihmucuk/leash
23
+ leash --setup <platform>
24
+ ```
25
+
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` |
32
+
33
+ Restart your agent. Done!
34
+
35
+ ```bash
36
+ # Update anytime
37
+ npm update -g @melihmucuk/leash
38
+
39
+ # Remove from a platform
40
+ leash --remove <platform>
41
+ ```
42
+
43
+ <details>
44
+ <summary><b>Manual Setup</b></summary>
45
+
46
+ If you prefer manual configuration, use `leash --path <platform>` to get the path and add it to your config file.
47
+
48
+ **Pi Coding Agent** - [docs](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/hooks.md)
49
+
50
+ Add to `~/.pi/agent/settings.json`:
51
+
52
+ ```json
53
+ {
54
+ "hooks": ["<path from leash --path pi>"]
55
+ }
56
+ ```
57
+
58
+ **OpenCode** - [docs](https://opencode.ai/docs/plugins/)
59
+
60
+ Add to `~/.config/opencode/config.json`:
61
+
62
+ ```json
63
+ {
64
+ "plugins": ["<path from leash --path opencode>"]
65
+ }
66
+ ```
67
+
68
+ **Claude Code** - [docs](https://code.claude.com/docs/en/hooks-guide)
69
+
70
+ Add to `~/.claude/settings.json`:
71
+
72
+ ```json
73
+ {
74
+ "hooks": {
75
+ "PreToolUse": [
76
+ {
77
+ "matcher": "Bash|Write|Edit",
78
+ "hooks": [
79
+ {
80
+ "type": "command",
81
+ "command": "node <path from leash --path claude-code>"
82
+ }
83
+ ]
84
+ }
85
+ ]
86
+ }
87
+ }
88
+ ```
89
+
90
+ **Factory Droid** - [docs](https://docs.factory.ai/cli/configuration/hooks-guide)
91
+
92
+ Add to `~/.factory/settings.json`:
93
+
94
+ ```json
95
+ {
96
+ "hooks": {
97
+ "PreToolUse": [
98
+ {
99
+ "matcher": "Execute|Write|Edit",
100
+ "hooks": [
101
+ {
102
+ "type": "command",
103
+ "command": "node <path from leash --path factory>"
104
+ }
105
+ ]
106
+ }
107
+ ]
108
+ }
109
+ }
110
+ ```
111
+
112
+ </details>
113
+
114
+ ## What Gets Blocked
115
+
116
+ ```bash
117
+ # Dangerous commands outside working directory
118
+ rm -rf ~/Documents # ❌ Delete outside working dir
119
+ mv ~/.bashrc /tmp/ # ❌ Move from outside
120
+ echo "data" > ~/file.txt # ❌ Redirect to home
121
+
122
+ # Protected files (blocked even inside project)
123
+ rm .env # ❌ Protected file
124
+ echo "SECRET=x" > .env.local # ❌ Protected file
125
+ rm -rf .git # ❌ Protected directory
126
+
127
+ # Dangerous git commands (blocked everywhere)
128
+ git reset --hard # ❌ Destroys uncommitted changes
129
+ git push --force # ❌ Destroys remote history
130
+ git clean -fd # ❌ Removes untracked files
131
+
132
+ # File operations via Write/Edit tools
133
+ ~/.bashrc # ❌ Home directory file
134
+ ../../../etc/hosts # ❌ Path traversal
135
+ .env # ❌ Protected file
136
+ ```
137
+
138
+ ## What's Allowed
139
+
140
+ ```bash
141
+ rm -rf ./node_modules # ✅ Working directory
142
+ rm -rf /tmp/build-cache # ✅ Temp directory
143
+ rm .env.example # ✅ Example files allowed
144
+ git commit -m "message" # ✅ Safe git commands
145
+ git push origin main # ✅ Normal push (no --force)
146
+ ```
147
+
148
+ <details>
149
+
150
+ <summary><b>Detailed Examples</b></summary>
151
+
152
+ ### Dangerous Commands
153
+
154
+ ```bash
155
+ rm -rf ~/Documents # ❌ Delete outside working dir
156
+ mv ~/.bashrc /tmp/ # ❌ Move from outside
157
+ cp ./secrets ~/leaked # ❌ Copy to outside
158
+ chmod 777 /etc/hosts # ❌ Permission change outside
159
+ chown user ~/file # ❌ Ownership change outside
160
+ ln -s ./file ~/link # ❌ Symlink to outside
161
+ dd if=/dev/zero of=~/file # ❌ Write outside
162
+ truncate -s 0 ~/file # ❌ Truncate outside
163
+ ```
164
+
165
+ ### Dangerous Git Commands
166
+
167
+ ```bash
168
+ git checkout -- . # ❌ Discards uncommitted changes
169
+ git restore src/file.ts # ❌ Discards uncommitted changes
170
+ git reset --hard # ❌ Destroys all uncommitted changes
171
+ git reset --hard HEAD~1 # ❌ Destroys commits and changes
172
+ git reset --merge # ❌ Can lose uncommitted changes
173
+ git clean -f # ❌ Removes untracked files permanently
174
+ git clean -fd # ❌ Removes untracked files and directories
175
+ git push --force # ❌ Destroys remote history
176
+ git push -f origin main # ❌ Destroys remote history
177
+ git branch -D feature # ❌ Force-deletes branch without merge check
178
+ git stash drop # ❌ Permanently deletes stashed changes
179
+ git stash clear # ❌ Deletes ALL stashed changes
180
+ ```
181
+
182
+ ### Redirects
183
+
184
+ ```bash
185
+ echo "data" > ~/file.txt # ❌ Redirect to home
186
+ echo "log" >> ~/app.log # ❌ Append to home
187
+ cat secrets > "/tmp/../~/x" # ❌ Path traversal in redirect
188
+ ```
189
+
190
+ ### Command Chains
191
+
192
+ ```bash
193
+ echo ok && rm ~/file # ❌ Dangerous command after &&
194
+ false || rm -rf ~/ # ❌ Dangerous command after ||
195
+ ls; rm ~/file # ❌ Dangerous command after ;
196
+ cat x | rm ~/file # ❌ Dangerous command in pipe
197
+ cd ~/Downloads && rm file # ❌ cd outside + dangerous command
198
+ cd .. && cd .. && rm target # ❌ cd hops escaping working dir
199
+ ```
200
+
201
+ ### Wrapper Commands
202
+
203
+ ```bash
204
+ sudo rm -rf ~/dir # ❌ sudo + dangerous command
205
+ env rm ~/file # ❌ env + dangerous command
206
+ command rm ~/file # ❌ command + dangerous command
207
+ ```
208
+
209
+ ### Compound Patterns
210
+
211
+ ```bash
212
+ find ~ -name "*.tmp" -delete # ❌ find -delete outside
213
+ find ~ -exec rm {} \; # ❌ find -exec rm outside
214
+ find ~/logs | xargs rm # ❌ xargs rm outside
215
+ find ~ | xargs -I{} mv {} /tmp # ❌ xargs mv outside
216
+ rsync -av --delete ~/src/ ~/dst/ # ❌ rsync --delete outside
217
+ ```
218
+
219
+ ### Protected Files (blocked even inside project)
220
+
221
+ ```bash
222
+ rm .env # ❌ Environment file
223
+ rm .env.local # ❌ Environment file
224
+ rm .env.production # ❌ Environment file
225
+ echo "x" > .env # ❌ Write to env file
226
+ rm -rf .git # ❌ Git directory
227
+ echo "x" > .git/config # ❌ Write to git directory
228
+ find . -name ".env" -delete # ❌ Delete protected via find
229
+ ```
230
+
231
+ Note: `.env.example` is allowed (template files are safe).
232
+
233
+ ### File Operations (Write/Edit tools)
234
+
235
+ ```bash
236
+ /etc/passwd # ❌ System file
237
+ ~/.bashrc # ❌ Home directory file
238
+ /home/user/.ssh/id_rsa # ❌ Absolute path outside
239
+ ../../../etc/hosts # ❌ Path traversal
240
+ .env # ❌ Protected file
241
+ .git/config # ❌ Protected directory
242
+ ```
243
+
244
+ ### What's Allowed (Full List)
245
+
246
+ ```bash
247
+ # Working directory operations
248
+ rm -rf ./node_modules
249
+ mv ./old.ts ./new.ts
250
+ cp ./src/config.json ./dist/
251
+ find . -name "*.bak" -delete
252
+ find ./logs | xargs rm
253
+
254
+ # Temp directory operations
255
+ rm -rf /tmp/build-cache
256
+ echo "data" > /tmp/output.txt
257
+ rsync -av --delete ./src/ /tmp/backup/
258
+
259
+ # Device paths
260
+ echo "x" > /dev/null
261
+ truncate -s 0 /dev/null
262
+
263
+ # Read from anywhere (safe)
264
+ cp /etc/hosts ./local-hosts
265
+ cat /etc/passwd
266
+
267
+ # Safe git commands
268
+ git status
269
+ git add .
270
+ git commit -m "message"
271
+ git push origin main
272
+ git checkout main
273
+ git checkout -b feature/new
274
+ git branch -d merged-branch # lowercase -d is safe
275
+ git reset --soft HEAD~1 # soft reset is safe
276
+ git restore --staged . # unstaging is safe
277
+ git stash
278
+ git stash pop
279
+ ```
280
+
281
+ </details>
282
+
283
+ ## Performance
284
+
285
+ Near-zero latency impact on your workflow:
286
+
287
+ | Platform | Latency per tool call | Notes |
288
+ | ----------- | --------------------- | ---------------------------------------- |
289
+ | OpenCode | **~20µs** | In-process plugin, near-zero overhead |
290
+ | Pi | **~20µs** | In-process hook, near-zero overhead |
291
+ | Claude Code | **~31ms** | External process (~30ms Node.js startup) |
292
+ | Factory | **~31ms** | External process (~30ms Node.js startup) |
293
+
294
+ For context: LLM API calls typically take 2-10+ seconds. Even the slower external process hook adds less than 0.3% to total response time.
295
+
296
+ ## Limitations
297
+
298
+ Leash is a **defense-in-depth** layer, not a complete sandbox. It cannot protect against:
299
+
300
+ - Kernel exploits or privilege escalation
301
+ - Network-based attacks (downloading and executing scripts)
302
+ - Commands not routed through the intercepted tools
303
+
304
+ For maximum security, combine Leash with container isolation (Docker), user permission restrictions, or read-only filesystem mounts.
305
+
306
+ ## Development
307
+
308
+ ```bash
309
+ cd ~/leash
310
+ npm install
311
+ npm run build
312
+ ```
313
+
314
+ ## Contributing
315
+
316
+ Contributions are welcome! Areas where help is needed:
317
+
318
+ - [ ] Plugin for AMP Code
319
+
320
+ ---
321
+
322
+ _Keep your AI agents on a leash._
package/bin/leash.js ADDED
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from "fs";
4
+ import { dirname, join } from "path";
5
+ import { homedir } from "os";
6
+ import { fileURLToPath } from "url";
7
+ import { PLATFORMS, setupPlatform, removePlatform } from "./lib.js";
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+
11
+ function getDistPath() {
12
+ return join(__dirname, "..", "dist");
13
+ }
14
+
15
+ function getConfigPath(platformKey) {
16
+ const platform = PLATFORMS[platformKey];
17
+ return platform ? join(homedir(), platform.configPath) : null;
18
+ }
19
+
20
+ function getLeashPath(platformKey) {
21
+ const platform = PLATFORMS[platformKey];
22
+ return platform ? join(getDistPath(), platform.distPath) : null;
23
+ }
24
+
25
+ function setup(platformKey) {
26
+ const configPath = getConfigPath(platformKey);
27
+ const leashPath = getLeashPath(platformKey);
28
+
29
+ if (!configPath || !leashPath) {
30
+ console.error(`Unknown platform: ${platformKey}`);
31
+ console.error(`Available: ${Object.keys(PLATFORMS).join(", ")}`);
32
+ process.exit(1);
33
+ }
34
+
35
+ if (!existsSync(leashPath)) {
36
+ console.error(`Leash not found at: ${leashPath}`);
37
+ process.exit(1);
38
+ }
39
+
40
+ const result = setupPlatform(platformKey, configPath, leashPath);
41
+
42
+ if (result.error) {
43
+ console.error(result.error);
44
+ process.exit(1);
45
+ }
46
+
47
+ if (result.skipped) {
48
+ console.log(`[ok] Leash already installed for ${result.platform}`);
49
+ return;
50
+ }
51
+
52
+ console.log(`[ok] Config: ${result.configPath}`);
53
+ console.log(`[ok] Leash installed for ${result.platform}`);
54
+ console.log(`[ok] Restart ${result.platform} to apply changes`);
55
+ }
56
+
57
+ function remove(platformKey) {
58
+ const configPath = getConfigPath(platformKey);
59
+
60
+ if (!configPath) {
61
+ console.error(`Unknown platform: ${platformKey}`);
62
+ console.error(`Available: ${Object.keys(PLATFORMS).join(", ")}`);
63
+ process.exit(1);
64
+ }
65
+
66
+ const result = removePlatform(platformKey, configPath);
67
+
68
+ if (result.error) {
69
+ console.error(result.error);
70
+ process.exit(1);
71
+ }
72
+
73
+ if (result.notFound) {
74
+ console.log(`[ok] No config found for ${result.platform}`);
75
+ return;
76
+ }
77
+
78
+ if (result.notInstalled) {
79
+ console.log(`[ok] Leash not found in ${result.platform} config`);
80
+ return;
81
+ }
82
+
83
+ console.log(`[ok] Leash removed from ${result.platform}`);
84
+ console.log(`[ok] Restart ${result.platform} to apply changes`);
85
+ }
86
+
87
+ function showPath(platformKey) {
88
+ const leashPath = getLeashPath(platformKey);
89
+
90
+ if (!leashPath) {
91
+ console.error(`Unknown platform: ${platformKey}`);
92
+ console.error(`Available: ${Object.keys(PLATFORMS).join(", ")}`);
93
+ process.exit(1);
94
+ }
95
+
96
+ console.log(leashPath);
97
+ }
98
+
99
+ function showHelp() {
100
+ console.log(`
101
+ leash - Security guardrails for AI coding agents
102
+
103
+ Usage:
104
+ leash --setup <platform> Install leash for a platform
105
+ leash --remove <platform> Remove leash from a platform
106
+ leash --path <platform> Show leash path for a platform
107
+ leash --help Show this help
108
+
109
+ Platforms:
110
+ opencode OpenCode
111
+ pi Pi Coding Agent
112
+ claude-code Claude Code
113
+ factory Factory Droid
114
+
115
+ Examples:
116
+ leash --setup opencode
117
+ leash --remove claude-code
118
+ leash --path pi
119
+ `);
120
+ }
121
+
122
+ const args = process.argv.slice(2);
123
+ const command = args[0];
124
+ const platform = args[1];
125
+
126
+ switch (command) {
127
+ case "--setup":
128
+ case "-s":
129
+ if (!platform) {
130
+ console.error("Missing platform argument");
131
+ showHelp();
132
+ process.exit(1);
133
+ }
134
+ setup(platform);
135
+ break;
136
+ case "--remove":
137
+ case "-r":
138
+ if (!platform) {
139
+ console.error("Missing platform argument");
140
+ showHelp();
141
+ process.exit(1);
142
+ }
143
+ remove(platform);
144
+ break;
145
+ case "--path":
146
+ case "-p":
147
+ if (!platform) {
148
+ console.error("Missing platform argument");
149
+ showHelp();
150
+ process.exit(1);
151
+ }
152
+ showPath(platform);
153
+ break;
154
+ case "--help":
155
+ case "-h":
156
+ case undefined:
157
+ showHelp();
158
+ break;
159
+ default:
160
+ console.error(`Unknown command: ${command}`);
161
+ showHelp();
162
+ process.exit(1);
163
+ }
package/bin/lib.js ADDED
@@ -0,0 +1,156 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { dirname } from "path";
3
+
4
+ export const PLATFORMS = {
5
+ opencode: {
6
+ name: "OpenCode",
7
+ configPath: ".config/opencode/config.json",
8
+ distPath: "opencode/leash.js",
9
+ setup: (config, leashPath) => {
10
+ config.plugins = config.plugins || [];
11
+ if (config.plugins.some((p) => p.includes("leash"))) {
12
+ return { skipped: true };
13
+ }
14
+ config.plugins.push(leashPath);
15
+ return { skipped: false };
16
+ },
17
+ remove: (config) => {
18
+ if (!config.plugins) return false;
19
+ const before = config.plugins.length;
20
+ config.plugins = config.plugins.filter((p) => !p.includes("leash"));
21
+ return config.plugins.length < before;
22
+ },
23
+ },
24
+ pi: {
25
+ name: "Pi",
26
+ configPath: ".pi/agent/settings.json",
27
+ distPath: "pi/leash.js",
28
+ setup: (config, leashPath) => {
29
+ config.hooks = config.hooks || [];
30
+ if (config.hooks.some((h) => h.includes("leash"))) {
31
+ return { skipped: true };
32
+ }
33
+ config.hooks.push(leashPath);
34
+ return { skipped: false };
35
+ },
36
+ remove: (config) => {
37
+ if (!config.hooks) return false;
38
+ const before = config.hooks.length;
39
+ config.hooks = config.hooks.filter((h) => !h.includes("leash"));
40
+ return config.hooks.length < before;
41
+ },
42
+ },
43
+ "claude-code": {
44
+ name: "Claude Code",
45
+ configPath: ".claude/settings.json",
46
+ distPath: "claude-code/leash.js",
47
+ setup: (config, leashPath) => {
48
+ config.hooks = config.hooks || {};
49
+ config.hooks.PreToolUse = config.hooks.PreToolUse || [];
50
+ const exists = config.hooks.PreToolUse.some((entry) =>
51
+ entry.hooks?.some((h) => h.command?.includes("leash"))
52
+ );
53
+ if (exists) {
54
+ return { skipped: true };
55
+ }
56
+ config.hooks.PreToolUse.push({
57
+ matcher: "Bash|Write|Edit",
58
+ hooks: [{ type: "command", command: `node ${leashPath}` }],
59
+ });
60
+ return { skipped: false };
61
+ },
62
+ remove: (config) => {
63
+ if (!config.hooks?.PreToolUse) return false;
64
+ const before = config.hooks.PreToolUse.length;
65
+ config.hooks.PreToolUse = config.hooks.PreToolUse.filter(
66
+ (entry) => !entry.hooks?.some((h) => h.command?.includes("leash"))
67
+ );
68
+ return config.hooks.PreToolUse.length < before;
69
+ },
70
+ },
71
+ factory: {
72
+ name: "Factory",
73
+ configPath: ".factory/settings.json",
74
+ distPath: "factory/leash.js",
75
+ setup: (config, leashPath) => {
76
+ config.hooks = config.hooks || {};
77
+ config.hooks.PreToolUse = config.hooks.PreToolUse || [];
78
+ const exists = config.hooks.PreToolUse.some((entry) =>
79
+ entry.hooks?.some((h) => h.command?.includes("leash"))
80
+ );
81
+ if (exists) {
82
+ return { skipped: true };
83
+ }
84
+ config.hooks.PreToolUse.push({
85
+ matcher: "Execute|Write|Edit",
86
+ hooks: [{ type: "command", command: `node ${leashPath}` }],
87
+ });
88
+ return { skipped: false };
89
+ },
90
+ remove: (config) => {
91
+ if (!config.hooks?.PreToolUse) return false;
92
+ const before = config.hooks.PreToolUse.length;
93
+ config.hooks.PreToolUse = config.hooks.PreToolUse.filter(
94
+ (entry) => !entry.hooks?.some((h) => h.command?.includes("leash"))
95
+ );
96
+ return config.hooks.PreToolUse.length < before;
97
+ },
98
+ },
99
+ };
100
+
101
+ export function readConfig(configPath) {
102
+ if (!existsSync(configPath)) {
103
+ return {};
104
+ }
105
+ try {
106
+ return JSON.parse(readFileSync(configPath, "utf-8"));
107
+ } catch {
108
+ return {};
109
+ }
110
+ }
111
+
112
+ export function writeConfig(configPath, config) {
113
+ const dir = dirname(configPath);
114
+ if (!existsSync(dir)) {
115
+ mkdirSync(dir, { recursive: true });
116
+ }
117
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
118
+ }
119
+
120
+ export function setupPlatform(platformKey, configPath, leashPath) {
121
+ const platform = PLATFORMS[platformKey];
122
+ if (!platform) {
123
+ return { error: `Unknown platform: ${platformKey}` };
124
+ }
125
+
126
+ const config = readConfig(configPath);
127
+ const result = platform.setup(config, leashPath);
128
+
129
+ if (result.skipped) {
130
+ return { skipped: true, platform: platform.name };
131
+ }
132
+
133
+ writeConfig(configPath, config);
134
+ return { success: true, platform: platform.name, configPath };
135
+ }
136
+
137
+ export function removePlatform(platformKey, configPath) {
138
+ const platform = PLATFORMS[platformKey];
139
+ if (!platform) {
140
+ return { error: `Unknown platform: ${platformKey}` };
141
+ }
142
+
143
+ if (!existsSync(configPath)) {
144
+ return { notFound: true, platform: platform.name };
145
+ }
146
+
147
+ const config = readConfig(configPath);
148
+ const removed = platform.remove(config);
149
+
150
+ if (!removed) {
151
+ return { notInstalled: true, platform: platform.name };
152
+ }
153
+
154
+ writeConfig(configPath, config);
155
+ return { success: true, platform: platform.name };
156
+ }