@aliou/pi-guardrails 0.10.0 → 0.11.1

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
@@ -1,3 +1,5 @@
1
+ ![banner](https://assets.aliou.me/pi-extensions/banners/pi-guardrails.png)
2
+
1
3
  # Guardrails
2
4
 
3
5
  Security hooks for Pi to reduce accidental destructive actions and secret-file access.
@@ -27,6 +29,7 @@ pi install git:github.com/aliou/pi-guardrails
27
29
 
28
30
  - **policies**: named file-protection rules with per-rule protection levels.
29
31
  - **permission-gate**: detects dangerous bash commands and asks for confirmation.
32
+ - **path-access**: restricts tool access to the current working directory with allow/ask/block modes.
30
33
  - **optional command explainer**: can call a small LLM to explain a dangerous command inline in the confirmation dialog.
31
34
 
32
35
  ## Config locations
@@ -48,7 +51,12 @@ Use `/guardrails:settings` to edit config interactively.
48
51
  "enabled": true,
49
52
  "features": {
50
53
  "policies": true,
51
- "permissionGate": true
54
+ "permissionGate": true,
55
+ "pathAccess": false
56
+ },
57
+ "pathAccess": {
58
+ "mode": "ask",
59
+ "allowedPaths": []
52
60
  },
53
61
  "policies": {
54
62
  "rules": [
@@ -121,6 +129,31 @@ Use:
121
129
 
122
130
  This starts a subagent that helps build and save one policy rule.
123
131
 
132
+ ## Path access
133
+
134
+ Restrict tool access to the current working directory. When enabled, any tool call targeting a path outside `cwd` is checked against the configured mode:
135
+
136
+ - **allow**: no restrictions
137
+ - **ask**: prompt with options to grant access (file or directory, for session or always)
138
+ - **block**: deny all outside access
139
+
140
+ ```jsonc
141
+ {
142
+ "features": { "pathAccess": true },
143
+ "pathAccess": {
144
+ "mode": "ask",
145
+ "allowedPaths": ["~/code/shared-libs/", "~/.config/myapp"]
146
+ }
147
+ }
148
+ ```
149
+
150
+ Grants are stored in project config (always) or session memory (session). The `allowedPaths` array is merged across all config scopes.
151
+
152
+ Limitations:
153
+ - Symlinks are not resolved (lexical path comparison only).
154
+ - Bash path extraction is best-effort (AST-based heuristics).
155
+ - In non-interactive mode, `ask` mode degrades to `block`.
156
+
124
157
  ## Permission gate
125
158
 
126
159
  Detects dangerous bash commands and prompts user confirmation.
@@ -161,6 +194,16 @@ Also note:
161
194
 
162
195
  - `preventBrew`, `preventPython`, `enforcePackageManager`, `packageManager` were removed from guardrails and moved to `@aliou/pi-toolchain`.
163
196
 
197
+ ## Development
198
+
199
+ ```bash
200
+ pnpm test # Run tests
201
+ pnpm test:watch # Run tests in watch mode
202
+ pnpm typecheck # Type check
203
+ pnpm lint # Lint
204
+ pnpm format # Format
205
+ ```
206
+
164
207
  ## Events
165
208
 
166
209
  Guardrails emits events for other extensions:
@@ -169,7 +212,7 @@ Guardrails emits events for other extensions:
169
212
 
170
213
  ```ts
171
214
  interface GuardrailsBlockedEvent {
172
- feature: "policies" | "permissionGate";
215
+ feature: "policies" | "permissionGate" | "pathAccess";
173
216
  toolName: string;
174
217
  input: Record<string, unknown>;
175
218
  reason: string;
@@ -185,4 +228,4 @@ interface GuardrailsDangerousEvent {
185
228
  description: string;
186
229
  pattern: string;
187
230
  }
188
- ```
231
+ ```
package/docs/defaults.md CHANGED
@@ -101,6 +101,31 @@ Blocks access to GPG/GnuPG private keys, keyrings, and configuration. Disabled b
101
101
 
102
102
  ---
103
103
 
104
+ ## Path Access
105
+
106
+ | Setting | Default |
107
+ |---|---|
108
+ | `features.pathAccess` | `false` |
109
+ | `pathAccess.mode` | `"ask"` |
110
+ | `pathAccess.allowedPaths` | `[]` |
111
+
112
+ Modes:
113
+ - `allow` — no path restrictions
114
+ - `ask` — prompt when accessing paths outside working directory
115
+ - `block` — deny all access outside working directory
116
+
117
+ Allowed paths use trailing-slash convention:
118
+ - `/path/to/file` — exact file match
119
+ - `/path/to/dir/` — directory and all descendants
120
+ - Supports `~/` for home directory
121
+
122
+ Limitations:
123
+ - Bash path extraction is best-effort (AST-based heuristics). Tokens like `application/json` may trigger false-positive prompts.
124
+ - Symlinks are not resolved. Lexical path comparison only.
125
+ - In non-interactive mode (--print), `ask` mode degrades to `block`.
126
+
127
+ ---
128
+
104
129
  ## Default Permission Gate Patterns
105
130
 
106
131
  These commands are detected using AST-based structural matching for accuracy.
@@ -109,7 +134,7 @@ These commands are detected using AST-based structural matching for accuracy.
109
134
  |-----------------|--------------------------------|
110
135
  | `rm -rf` | Recursive force delete |
111
136
  | `sudo` | Superuser command |
112
- | `dd if=` | Disk write operation |
137
+ | `dd of=` | Disk write operation |
113
138
  | `mkfs.` | Filesystem format |
114
139
  | `chmod -R 777` | Insecure recursive permissions |
115
140
  | `chown -R` | Recursive ownership change |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-guardrails",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -49,7 +49,8 @@
49
49
  "@sinclair/typebox": "^0.34.48",
50
50
  "@types/node": "^25.0.10",
51
51
  "husky": "^9.1.7",
52
- "typescript": "^5.9.3"
52
+ "typescript": "^5.9.3",
53
+ "vitest": "^4.1.4"
53
54
  },
54
55
  "peerDependenciesMeta": {
55
56
  "@mariozechner/pi-agent-core": {
@@ -67,6 +68,8 @@
67
68
  },
68
69
  "scripts": {
69
70
  "typecheck": "tsc --noEmit",
71
+ "test": "vitest run",
72
+ "test:watch": "vitest",
70
73
  "lint": "biome check",
71
74
  "format": "biome check --write",
72
75
  "check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts",
@@ -10,12 +10,25 @@ import {
10
10
  function mergeOnboarding(
11
11
  base: GuardrailsConfig | null,
12
12
  applyBuiltinDefaults: boolean,
13
+ pathAccessEnabled?: boolean | null,
13
14
  ): GuardrailsConfig {
14
15
  const next = structuredClone(base ?? {});
15
- const onboarded = buildOnboardedConfig(applyBuiltinDefaults);
16
+ const onboarded = buildOnboardedConfig(
17
+ applyBuiltinDefaults,
18
+ pathAccessEnabled,
19
+ );
16
20
  next.applyBuiltinDefaults = onboarded.applyBuiltinDefaults;
17
21
  next.version = onboarded.version;
18
22
  next.onboarding = onboarded.onboarding;
23
+ if (onboarded.features?.pathAccess !== undefined) {
24
+ next.features = {
25
+ ...next.features,
26
+ pathAccess: onboarded.features.pathAccess,
27
+ };
28
+ }
29
+ if (onboarded.pathAccess) {
30
+ next.pathAccess = onboarded.pathAccess;
31
+ }
19
32
  return next;
20
33
  }
21
34
 
@@ -48,7 +61,11 @@ export function registerGuardrailsOnboardingCommand(
48
61
  return;
49
62
  }
50
63
 
51
- const merged = mergeOnboarding(globalConfig, result.applyBuiltinDefaults);
64
+ const merged = mergeOnboarding(
65
+ globalConfig,
66
+ result.applyBuiltinDefaults,
67
+ result.pathAccessEnabled,
68
+ );
52
69
  await configLoader.save("global", merged);
53
70
  await configLoader.load();
54
71
 
@@ -11,11 +11,13 @@ import { CURRENT_VERSION } from "../utils/migration";
11
11
 
12
12
  interface OnboardingState {
13
13
  applyBuiltinDefaults: boolean | null;
14
+ pathAccessEnabled: boolean | null;
14
15
  }
15
16
 
16
17
  export interface OnboardingResult {
17
18
  completed: boolean;
18
19
  applyBuiltinDefaults: boolean | null;
20
+ pathAccessEnabled: boolean | null;
19
21
  }
20
22
 
21
23
  class IntroStep implements Component {
@@ -68,7 +70,7 @@ class DefaultsChoiceStep implements Component {
68
70
  "",
69
71
  "- Protect secret files like `.env`, `.env.local`, `.env.production`, and `.dev.vars`",
70
72
  "- Keep safe exceptions like `.env.example` and `*.sample.env`",
71
- "- Require confirmation before running dangerous commands like `rm -rf`, `sudo`, and `dd if=`",
73
+ "- Require confirmation before running dangerous commands like `rm -rf`, `sudo`, and `dd of=`",
72
74
  ].join("\n"),
73
75
  [
74
76
  "Start with no built-in file policy defaults.",
@@ -129,6 +131,88 @@ class DefaultsChoiceStep implements Component {
129
131
  }
130
132
  }
131
133
 
134
+ class PathAccessStep implements Component {
135
+ private selectedIndex = 0;
136
+ private readonly settingsTheme: SettingsTheme;
137
+
138
+ constructor(
139
+ private readonly theme: Theme,
140
+ private readonly state: OnboardingState,
141
+ private readonly onSelect: () => void,
142
+ ) {
143
+ this.settingsTheme = getSettingsTheme(theme);
144
+ }
145
+
146
+ invalidate() {}
147
+
148
+ render(width: number): string[] {
149
+ const options = ["Ask before accessing outside files", "No restrictions"];
150
+ const explanations = [
151
+ [
152
+ "When enabled, guardrails will prompt you before the agent accesses files outside the current working directory.",
153
+ "",
154
+ "- You can grant access per-file or per-directory",
155
+ "- Grants can be session-only or permanent",
156
+ "- In non-interactive mode, outside access is blocked",
157
+ ].join("\n"),
158
+ [
159
+ "The agent can access any path on your system without prompting.",
160
+ "",
161
+ "- You can enable path access later in `/guardrails:settings`",
162
+ ].join("\n"),
163
+ ];
164
+
165
+ const lines: string[] = [
166
+ " Restrict access to your project directory?",
167
+ "",
168
+ ];
169
+
170
+ for (let i = 0; i < options.length; i++) {
171
+ const option = options[i];
172
+ if (!option) continue;
173
+ const selected = i === this.selectedIndex;
174
+ const prefix = selected ? this.settingsTheme.cursor : " ";
175
+ const label = this.settingsTheme.value(` ${option}`, selected);
176
+ lines.push(`${prefix}${label}`);
177
+ }
178
+
179
+ lines.push("");
180
+
181
+ const explanationBox = new Box(1, 0, (s: string) => s);
182
+ explanationBox.addChild(
183
+ new Markdown(
184
+ explanations[this.selectedIndex] ?? "",
185
+ 0,
186
+ 0,
187
+ getMarkdownTheme(),
188
+ {
189
+ color: (s: string) => this.theme.fg("text", s),
190
+ },
191
+ ),
192
+ );
193
+
194
+ lines.push(...explanationBox.render(Math.max(1, width)));
195
+
196
+ return lines;
197
+ }
198
+
199
+ handleInput(data: string): void {
200
+ if (matchesKey(data, Key.up) || data === "k") {
201
+ this.selectedIndex = this.selectedIndex === 0 ? 1 : 0;
202
+ return;
203
+ }
204
+ if (matchesKey(data, Key.down) || data === "j") {
205
+ this.selectedIndex = this.selectedIndex === 1 ? 0 : 1;
206
+ return;
207
+ }
208
+
209
+ if (matchesKey(data, Key.enter)) {
210
+ this.state.pathAccessEnabled = this.selectedIndex === 0;
211
+ this.onSelect();
212
+ }
213
+ }
214
+ }
215
+
132
216
  class FinishStep implements Component {
133
217
  private readonly recapMarkdown = new Markdown("", 2, 0, getMarkdownTheme());
134
218
 
@@ -142,7 +226,7 @@ class FinishStep implements Component {
142
226
  }
143
227
 
144
228
  render(width: number): string[] {
145
- const content =
229
+ const defaultsPart =
146
230
  this.state.applyBuiltinDefaults === true
147
231
  ? [
148
232
  "You selected **Recommended defaults**.",
@@ -150,7 +234,7 @@ class FinishStep implements Component {
150
234
  "Guardrails will start with built-in protection, including:",
151
235
  "- secret files like `.env`, `.env.local`, `.env.production`, `.dev.vars`",
152
236
  "- safe exceptions like `.env.example` and `*.sample.env`",
153
- "- confirmation before running dangerous commands like `rm -rf`, `sudo`, `dd if=`",
237
+ "- confirmation before running dangerous commands like `rm -rf`, `sudo`, `dd of=`",
154
238
  ].join("\n")
155
239
  : [
156
240
  "You selected **Minimal setup**.",
@@ -160,6 +244,12 @@ class FinishStep implements Component {
160
244
  "You can configure policies later with `/guardrails:settings`.",
161
245
  ].join("\n");
162
246
 
247
+ const pathAccessPart = this.state.pathAccessEnabled
248
+ ? "\n\n**Path access**: enabled (ask mode). The agent will prompt before accessing files outside the working directory."
249
+ : "\n\n**Path access**: disabled. No path restrictions.";
250
+
251
+ const content = defaultsPart + pathAccessPart;
252
+
163
253
  this.recapMarkdown.setText(content);
164
254
  return [...this.recapMarkdown.render(Math.max(1, width)), ""];
165
255
  }
@@ -177,6 +267,7 @@ export function createOnboardingWizard(
177
267
  ): Component {
178
268
  const state: OnboardingState = {
179
269
  applyBuiltinDefaults: null,
270
+ pathAccessEnabled: null,
180
271
  };
181
272
 
182
273
  let markWelcomeComplete: (() => void) | null = null;
@@ -210,6 +301,14 @@ export function createOnboardingWizard(
210
301
  ctx.goNext();
211
302
  }),
212
303
  },
304
+ {
305
+ label: "Path access",
306
+ build: (ctx) =>
307
+ new PathAccessStep(theme, state, () => {
308
+ ctx.markComplete();
309
+ ctx.goNext();
310
+ }),
311
+ },
213
312
  {
214
313
  label: "Recap",
215
314
  build: (ctx) =>
@@ -219,21 +318,32 @@ export function createOnboardingWizard(
219
318
  finalize({
220
319
  completed: true,
221
320
  applyBuiltinDefaults: state.applyBuiltinDefaults,
321
+ pathAccessEnabled: state.pathAccessEnabled,
222
322
  });
223
323
  }),
224
324
  },
225
325
  ],
226
326
  onComplete: () => {
227
327
  if (state.applyBuiltinDefaults === null) {
228
- finalize({ completed: false, applyBuiltinDefaults: null });
328
+ finalize({
329
+ completed: false,
330
+ applyBuiltinDefaults: null,
331
+ pathAccessEnabled: null,
332
+ });
229
333
  return;
230
334
  }
231
335
  finalize({
232
336
  completed: true,
233
337
  applyBuiltinDefaults: state.applyBuiltinDefaults,
338
+ pathAccessEnabled: state.pathAccessEnabled,
234
339
  });
235
340
  },
236
- onCancel: () => finalize({ completed: false, applyBuiltinDefaults: null }),
341
+ onCancel: () =>
342
+ finalize({
343
+ completed: false,
344
+ applyBuiltinDefaults: null,
345
+ pathAccessEnabled: null,
346
+ }),
237
347
  hintSuffix: "Enter select/continue",
238
348
  minContentHeight: 12,
239
349
  });
@@ -256,8 +366,9 @@ export function createOnboardingWizard(
256
366
 
257
367
  export function buildOnboardedConfig(
258
368
  applyBuiltinDefaults: boolean,
369
+ pathAccessEnabled?: boolean | null,
259
370
  ): GuardrailsConfig {
260
- return {
371
+ const config: GuardrailsConfig = {
261
372
  version: CURRENT_VERSION,
262
373
  applyBuiltinDefaults,
263
374
  onboarding: {
@@ -266,6 +377,11 @@ export function buildOnboardedConfig(
266
377
  version: CURRENT_VERSION,
267
378
  },
268
379
  };
380
+ if (pathAccessEnabled) {
381
+ config.features = { ...config.features, pathAccess: true };
382
+ config.pathAccess = { mode: "ask" };
383
+ }
384
+ return config;
269
385
  }
270
386
 
271
387
  export function isOnboardingPending(config: GuardrailsConfig | null): boolean {
@@ -27,6 +27,7 @@ import type {
27
27
  ResolvedConfig,
28
28
  } from "../config";
29
29
  import { configLoader } from "../config";
30
+ import { normalizeAllowedPaths } from "../utils/migration";
30
31
 
31
32
  type FeatureKey = keyof ResolvedConfig["features"];
32
33
 
@@ -40,6 +41,10 @@ const FEATURE_UI: Record<FeatureKey, { label: string; description: string }> = {
40
41
  description:
41
42
  "Prompt for confirmation on dangerous commands (rm -rf, sudo, etc.)",
42
43
  },
44
+ pathAccess: {
45
+ label: "Path access",
46
+ description: "Restrict tool access to the current working directory",
47
+ },
43
48
  };
44
49
 
45
50
  const POLICY_EXAMPLES: Array<{
@@ -687,6 +692,157 @@ class AddRuleSubmenu implements Component {
687
692
  }
688
693
  }
689
694
 
695
+ class PathListEditor implements Component {
696
+ private readonly input = new Input();
697
+ private items: string[];
698
+ private selectedIndex = 0;
699
+ private mode: "list" | "add" | "edit" = "list";
700
+ private editIndex = -1;
701
+
702
+ constructor(
703
+ private readonly options: {
704
+ label: string;
705
+ items: string[];
706
+ theme: SettingsListTheme;
707
+ onSave: (items: string[]) => void;
708
+ onDone: () => void;
709
+ maxVisible?: number;
710
+ },
711
+ ) {
712
+ this.items = [...options.items];
713
+ this.input.onSubmit = () => this.submit();
714
+ this.input.onEscape = () => this.cancel();
715
+ }
716
+
717
+ invalidate() {}
718
+
719
+ render(width: number): string[] {
720
+ const lines = [
721
+ this.options.theme.label(` ${this.options.label}`, true),
722
+ "",
723
+ ];
724
+ if (this.mode === "add" || this.mode === "edit") {
725
+ lines.push(
726
+ this.options.theme.hint(
727
+ this.mode === "edit" ? " Edit path:" : " New path:",
728
+ ),
729
+ "",
730
+ ...this.input.render(Math.max(1, width - 4)).map((line) => ` ${line}`),
731
+ "",
732
+ this.options.theme.hint(" Enter: save · Esc: cancel"),
733
+ );
734
+ return lines;
735
+ }
736
+
737
+ if (this.items.length === 0) {
738
+ lines.push(this.options.theme.hint(" (empty)"));
739
+ } else {
740
+ const maxVisible = this.options.maxVisible ?? 10;
741
+ const startIndex = Math.max(
742
+ 0,
743
+ Math.min(
744
+ this.selectedIndex - Math.floor(maxVisible / 2),
745
+ this.items.length - maxVisible,
746
+ ),
747
+ );
748
+ const endIndex = Math.min(startIndex + maxVisible, this.items.length);
749
+ for (let i = startIndex; i < endIndex; i++) {
750
+ const item = this.items[i];
751
+ if (!item) continue;
752
+ const isSelected = i === this.selectedIndex;
753
+ const prefix = isSelected ? this.options.theme.cursor : " ";
754
+ lines.push(prefix + this.options.theme.value(item, isSelected));
755
+ }
756
+ if (startIndex > 0 || endIndex < this.items.length) {
757
+ lines.push(
758
+ this.options.theme.hint(
759
+ ` (${this.selectedIndex + 1}/${this.items.length})`,
760
+ ),
761
+ );
762
+ }
763
+ }
764
+
765
+ lines.push("");
766
+ lines.push(
767
+ this.options.theme.hint(
768
+ " a: add · e/Enter: edit · d: delete · Esc: back",
769
+ ),
770
+ );
771
+ return lines;
772
+ }
773
+
774
+ handleInput(data: string): void {
775
+ if (this.mode === "add" || this.mode === "edit") {
776
+ this.input.handleInput(data);
777
+ return;
778
+ }
779
+
780
+ if (matchesKey(data, Key.up) || data === "k") {
781
+ if (this.items.length === 0) return;
782
+ this.selectedIndex =
783
+ this.selectedIndex === 0
784
+ ? this.items.length - 1
785
+ : this.selectedIndex - 1;
786
+ } else if (matchesKey(data, Key.down) || data === "j") {
787
+ if (this.items.length === 0) return;
788
+ this.selectedIndex =
789
+ this.selectedIndex === this.items.length - 1
790
+ ? 0
791
+ : this.selectedIndex + 1;
792
+ } else if (data === "a" || data === "A") {
793
+ this.mode = "add";
794
+ this.input.setValue("");
795
+ } else if (data === "e" || data === "E" || matchesKey(data, Key.enter)) {
796
+ this.startEdit();
797
+ } else if (data === "d" || data === "D") {
798
+ this.deleteSelected();
799
+ } else if (matchesKey(data, Key.escape)) {
800
+ this.options.onDone();
801
+ }
802
+ }
803
+
804
+ private startEdit(): void {
805
+ const item = this.items[this.selectedIndex];
806
+ if (!item) return;
807
+ this.mode = "edit";
808
+ this.editIndex = this.selectedIndex;
809
+ this.input.setValue(item);
810
+ }
811
+
812
+ private submit(): void {
813
+ const path = this.input.getValue().trim();
814
+ if (!path) {
815
+ this.cancel();
816
+ return;
817
+ }
818
+
819
+ if (this.mode === "edit") {
820
+ this.items[this.editIndex] = path;
821
+ } else {
822
+ this.items.push(path);
823
+ this.selectedIndex = this.items.length - 1;
824
+ }
825
+ this.items = [...new Set(this.items)];
826
+ this.options.onSave([...this.items]);
827
+ this.cancel();
828
+ }
829
+
830
+ private deleteSelected(): void {
831
+ if (this.items.length === 0) return;
832
+ this.items.splice(this.selectedIndex, 1);
833
+ if (this.selectedIndex >= this.items.length) {
834
+ this.selectedIndex = Math.max(0, this.items.length - 1);
835
+ }
836
+ this.options.onSave([...this.items]);
837
+ }
838
+
839
+ private cancel(): void {
840
+ this.mode = "list";
841
+ this.editIndex = -1;
842
+ this.input.setValue("");
843
+ }
844
+ }
845
+
690
846
  class ScopePickerSubmenu implements Component {
691
847
  private selectedIndex = 0;
692
848
 
@@ -1063,6 +1219,23 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
1063
1219
  };
1064
1220
  }
1065
1221
 
1222
+ function pathListSubmenu(id: string, label: string) {
1223
+ return (_val: string, submenuDone: (v?: string) => void) => {
1224
+ const items = normalizeAllowedPaths(getNestedValue(scopedConfig, id));
1225
+ let latestCount = items.length;
1226
+ return new PathListEditor({
1227
+ label,
1228
+ items,
1229
+ theme: settingsTheme,
1230
+ onSave: (newItems) => {
1231
+ latestCount = newItems.length;
1232
+ applyDraft(id, newItems);
1233
+ },
1234
+ onDone: () => submenuDone(`${latestCount} items`),
1235
+ });
1236
+ };
1237
+ }
1238
+
1066
1239
  function patternConfigSubmenu(
1067
1240
  id: string,
1068
1241
  label: string,
@@ -1210,6 +1383,30 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
1210
1383
  label: `Policies (${policyRules.length})`,
1211
1384
  items: policyItems,
1212
1385
  },
1386
+ {
1387
+ label: "Path Access",
1388
+ items: [
1389
+ {
1390
+ id: "pathAccess.mode",
1391
+ label: "Mode",
1392
+ description:
1393
+ "allow: no restrictions, ask: prompt for outside paths, block: deny all outside paths",
1394
+ currentValue: scopedConfig.pathAccess?.mode ?? "(inherited)",
1395
+ values: ["allow", "ask", "block"],
1396
+ },
1397
+ {
1398
+ id: "pathAccess.allowedPaths",
1399
+ label: "Allowed paths",
1400
+ description:
1401
+ "Paths always allowed (trailing / for directories). Supports ~/",
1402
+ currentValue: count("pathAccess.allowedPaths"),
1403
+ submenu: pathListSubmenu(
1404
+ "pathAccess.allowedPaths",
1405
+ "Allowed Paths",
1406
+ ),
1407
+ },
1408
+ ],
1409
+ },
1213
1410
  {
1214
1411
  label: "Permission Gate",
1215
1412
  items: [